Skip to content

Exit Cost: What Leaving a Stack Actually Costs a Team

Teams price how fast a library gets them shipping. Thirty years of migrations says the number that decides the outcome is what removal costs.

· · 7 min read
Laptop on a desk showing a JavaScript project with an Express route file open

Quick Take

Adoption cost is easy to see and almost never the number that hurts. Exit cost is invisible on day one, arrives four years later, and lands on people who did not make the decision. Here is how to estimate it before you sign up.

Quick take: Adoption cost is what a library charges you to start. Exit cost is what it charges you to stop, and it is the number that decides whether a stack choice ages well. Estimate it with three questions: does the tool own a syntax, does it own your test assertions, and does it sit at the build boundary or the runtime boundary? Build-boundary dependencies (Create React App, Grunt, Bower) come out in days. Runtime-boundary dependencies (Moment.js, Enzyme, styled-components) come out in quarters.

I started writing code in 1995, and the single most expensive lesson from those thirty years has nothing to do with picking winners. I've picked plenty of winners. The bills that hurt came from tools that were genuinely good, genuinely popular, and genuinely correct choices at the time, and then had to come out anyway.

That's the part the evaluation spreadsheet never has a column for. We compare bundle size, benchmark numbers, GitHub stars, hiring pool. When did you last see a row labeled "days to remove"? Nobody writes down the removal estimate. So here's the argument: for any dependency you expect to keep longer than two years, exit cost is a better predictor of total spend than every adoption metric combined.

What Exit Cost Actually Measures

Not money. Not the license. Exit cost is the number of engineer-days between "this dependency has to go" and "this dependency is gone and the tests still pass." It has three components, and only one of them is obvious:

  • Call sites. How many files import it, directly or through something that does.
  • Assertions. How much of your test suite asserts against its behavior rather than yours.
  • Re-verification. How much of the replaced behavior nobody can prove was identical.

The third is where estimates go wrong. Swapping a date library is find-and-replace right up until you discover that three reports depended on an off-by-one the old library produced and the new one doesn't. I have shipped that bug. Twice, on two different date libraries, about eight years apart.

The Three Questions That Predict It

Does the tool own a syntax or a file format?

This is the most expensive category and the easiest to spot. CoffeeScript owned .coffee. Sass owns .scss. Vue owns .vue. styled-components owned a tagged template literal that is neither CSS nor JavaScript to any tool that isn't styled-components.

Owning a syntax means your source is no longer portable text. Removing it isn't a refactor, it's a transpile plus a review of whatever the transpile got subtly wrong. When the styled-components maintainer announced maintenance mode on 17 March 2025 and said plainly that he wouldn't recommend it for new projects, teams with 2,000 styled components had no upgrade path. They had a rewrite.

None of that makes Sass or Vue bad. It makes them expensive to leave, which is a different claim, and one worth pricing in.

Does the tool own your test assertions?

Enzyme is the clean case study here. It didn't just render components, it exposed a shallow-rendering model with its own wrapper API, so a mature suite contained thousands of assertions written in Enzyme's vocabulary rather than in the DOM's.

When React 18 shipped, no official Enzyme adapter followed. The author of the unofficial React 17 adapter published a post titled "Enzyme is dead" in December 2021 saying he wouldn't be writing one. There was nothing to install. Teams migrating to React Testing Library weren't swapping a dependency, they were rewriting every assertion, because the two libraries disagree about what a test should even look at.

Test frameworks look cheap because they don't ship to production. So why are they often the most expensive thing in the tree? Because assertions encode intent, and intent is the one thing a codemod can't carry across.

Build boundary or runtime boundary?

This one question sorts most of the field.

DependencyBoundaryWhat removal touchedTrigger and date
Create React AppBuildConfig, env prefixes, test setupSunset 14 Feb 2025
BowerBuildManifest, CI install stepSuperseded by npm and Yarn
GruntBuildTask definitions onlyDisplaced by npm scripts
Moment.jsRuntimeEvery parse, format, and compareLegacy status Sept 2020
EnzymeTest runtimeEvery component assertionNo React 18 adapter
ProtractorTest runtimeEvery end-to-end specEnd of life Aug 2023
AngularJSFramework runtimeEverythingLTS ended 31 Dec 2021

Build-boundary tools sit between your source and your artifact. Your components never mention them. The React team's own Create React App sunset notice points at Vite, Parcel, Rsbuild, or a framework, and teams finished those swaps in days.

Runtime-boundary tools are named in your source, so removing one means editing your source. Moment.js is the textbook example. Its maintainers declared it a legacy project in maintenance mode in September 2020, citing mutable objects and bundle size, and every project that had it had it everywhere. If you're doing that migration now, the Temporal API is where the standard landed, but the work is still call site by call site.

And AngularJS? Framework-runtime. Support ended 31 December 2021 after a six-month COVID extension, and "migrating" meant rewriting the application. Some shops are still paying commercial vendors to keep 1.x patched, which is exit cost expressed as a subscription.

The Ten-Minute Estimate

Before you run the install command, write the removal pull request description. Three lines:

  1. Which files change when this comes out?
  2. Which tests need new assertions, not just new imports?
  3. Who does it, and in which quarter?

If you can't answer all three in ten minutes, you don't know the exit cost, and unknown exit cost behaves exactly like high exit cost when the bill arrives. That's the whole technique, and it takes less time than reading the benchmark section of the README.

The follow-up trick is a wrapper. One module that owns the import, exposes the four functions you actually use, and gets mocked in tests. It costs an afternoon and it converts a runtime-boundary dependency into something closer to a build-boundary one. I've never regretted writing one. I have several times regretted the meeting where somebody argued a wrapper was over-engineering, and won.

The Opinion You Can Argue With

I'd take the slower, uglier library with a boring escape hatch over the faster one that owns my syntax. Every single time, without benchmarking either.

Most teams do the opposite and call it pragmatism, and I think they're wrong in a way that's hard to see because the cost lands on different people years later. The developer who chose the elegant tool got the productivity win and moved on. The developer paying the removal bill in 2030 never got a vote. If your tooling decisions are made by people who won't be there for the migration, exit cost isn't a technical criterion at all, it's a governance problem wearing a technical costume.

There's a real counterargument, and it deserves saying: TypeScript has enormous exit cost, and adopting it was still right. Types touch every file. Removing TypeScript from a large codebase is a genuine project. But it buys correctness at a scale nothing else does, and its output is plain JavaScript, so the escape hatch exists even if it's ugly. High exit cost is acceptable when the payoff is proportional. The failure mode isn't high exit cost, it's high exit cost bought for a marginal convenience.

Ask which one you're buying. Then ask how a package manager comparison or a flat config migration would look in the same terms, because tooling churn is where most teams discover their real answer.

What This Changes on Monday

Add one column to whatever comparison table you're building this week. Call it "removal estimate", fill it in with a number of days, and refuse to leave it blank. A blank cell there is the same as writing "unknown", and unknown is how a two-year-old dependency turns into a four-quarter migration nobody planned.

The tools I regret are not the ones that died. Things die. The ones I regret are the ones I couldn't put down.

Frequently Asked Questions

What is exit cost in software dependency terms?
Exit cost is the engineering work required to remove a dependency from a codebase after it stops being maintained or stops fitting. It is measured in call sites touched, tests rewritten, and behavior that has to be re-verified, not in license fees. A library used behind one wrapper module has near-zero exit cost. A library whose API appears in 900 files has an exit cost that can exceed the value it ever delivered.
How do I estimate exit cost before adopting a library?
Write the removal pull request description before you write the install command. Name the files that would change, the tests that would need new assertions, and the person who would do it. If you cannot fill in those three lines in ten minutes, the exit cost is unknown, which in practice means high. Then check whether the library owns a syntax, a file format, or a test API, because those three are what make removal expensive.
Is Create React App an example of high or low exit cost?
Low, relative to its blast radius. When the React team sunset Create React App on 14 February 2025, the recommended paths were Vite, Parcel, Rsbuild, or a framework. The migration touches build configuration, environment variable prefixes, and test setup, but it does not touch component code. That is the signature of a build-boundary dependency, and it is why teams finished those migrations in days rather than quarters.
Why was migrating off Moment.js so much harder than migrating off Create React App?
Because Moment sat at the runtime boundary. Its objects were mutable and its API appeared at every place a date was parsed, formatted, added to, or compared. When the maintainers declared it a legacy project in maintenance mode in September 2020, there was no adapter to install, only call sites to rewrite one at a time with behavior to re-verify at each one.
Does a large community reduce exit cost?
It reduces the risk of being stranded, not the cost of leaving. AngularJS had one of the largest communities in frontend history, and its long term support still ended on 31 December 2021. A big community usually means more of your code was written in that library's idioms, which makes the eventual removal larger, not smaller. Popularity is a survival signal. It is not an exit-cost signal.