Breaking Change Handling
Identify, classify, and absorb breaking changes from upstream releases without a fire drill.
Why
A breaking change is only breaking if it touches code you actually run; most teams over-react by rewriting everything the changelog mentions. The disciplined approach is to map each breaking change to the blast radius in your codebase — which entry points, which tests, which features — and absorb only those. This turns a scary "200 breaking changes" changelog into a tractable "7 of these touch our code" list. Classification also lets you sequence: pure renames are codemod-able and cheap, behavior changes need tests, removed APIs need replacements designed. Skipping classification is why upgrades take weeks instead of days.
When
During any major upgrade or when a dependency ships a breaking change in a minor (unfortunately common in pre-1.0 packages).
How
1. Extract every breaking change from the changelog into a spreadsheet: ID | description | type | blast radius 2. Classify type: rename (codemod) | removed-API (replacement) | behavior-change (test) | config-required (config) 3. grep your codebase for each removed/renamed symbol to measure blast radius; drop rows with zero hits 4. Sequence by risk: renames first (mechanical), then config, then removed-API, then behavior-change last 5. For each, write a failing test that demonstrates the breakage, then fix until green (TDD the upgrade) 6. Commit per breaking-change so git bisect can isolate any future regression to a specific change
Example
# Breaking-change tracker (markdown table in the upgrade PR)
| ID | Type | Symbol | Hits | Action |
|----|---------------|-------------------|------|---------------------|
| 1 | rename | ReactDOM.render | 3 | codemod -> createRoot |
| 2 | removed-API | componentWillMount | 0 | skip (zero hits) |
| 3 | behavior | useEffect run x2 | 12 | add StrictMode test |
| 4 | config | concurrent mode | 1 | set in root |
# Codemod the renames
npx @codemod/cli --plugin react-18 src/
# TDD a behavior change: StrictMode double-invokes effects
it("effect is idempotent under StrictMode", () => {
// ... fails before fix, passes after
});Common Problems
- ⚠Treating all breaking changes as equal — rewriting code that the changelog mentions but you don't actually call, wasting days.
- ⚠No blast-radius analysis — you discover a removed API at runtime in production because grep never ran.
- ⚠Fixing breaking changes in one mega-commit — git bisect is useless and a regression can't be isolated.
Best Practices
- ✓Always grep before fixing — a breaking change with zero hits in your code is free to skip, and most changelogs are 80% skippable.
- ✓Commit one breaking change per commit so bisect can pin any future regression to the exact upstream change.
- ✓Write a failing test first for each behavior-change item — the test documents the breakage and proves the fix.