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.
| Dependency | Boundary | What removal touched | Trigger and date |
|---|---|---|---|
| Create React App | Build | Config, env prefixes, test setup | Sunset 14 Feb 2025 |
| Bower | Build | Manifest, CI install step | Superseded by npm and Yarn |
| Grunt | Build | Task definitions only | Displaced by npm scripts |
| Moment.js | Runtime | Every parse, format, and compare | Legacy status Sept 2020 |
| Enzyme | Test runtime | Every component assertion | No React 18 adapter |
| Protractor | Test runtime | Every end-to-end spec | End of life Aug 2023 |
| AngularJS | Framework runtime | Everything | LTS 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:
- Which files change when this comes out?
- Which tests need new assertions, not just new imports?
- 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.