Skip to content
Back to Maintenance & LTS

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.