skip to content

A published library's public API compatibility promise under Semantic Versioning covers more than just function signatures. Besides removing or renaming an exported function, name two other kinds of changes that count as 'breaking' under SemVer and explain why each breaks the promise even though no code was deleted.

level: middleimportance: must knowfreq 85%

answer

  1. breakage = observable behavior change, not just deleted symbols
  2. default-value changes are breaking
  3. tightened validation is breaking
  4. Hyrum's Law - all observable behavior becomes API
  5. scope your public API explicitly to bound the promise

basics

~20 s

Breaking changes aren't just deleted functions. Changing what a function returns, tightening validation so old inputs now error, or changing a default behavior can all break callers even though the function still exists - because existing code that worked now behaves differently or fails.

solid answer

~40 s

SemVer's MAJOR bump covers any change that could break code correctly using the documented public API, not just deletions. Concrete non-deletion examples: (1) changing a return type or the shape/semantics of returned data, since callers pattern-match or serialize on the old shape; (2) tightening input validation so previously-accepted values now throw, since callers passing those values worked before; (3) changing a default value/behavior, since callers who relied on the default now get new behavior without touching their code. The common thread is 'does correctly-written existing code, unmodified, still behave the same way?' - if the answer is no, it's MAJOR regardless of whether any symbol was deleted.

go deeper

for a junior

Should recognize that removing or renaming an exported function is breaking, and guess with prompting that a changed default might also count.

for a middle

Should independently list 2-3 non-deletion categories of breaking change (default values, validation, return shape) and explain the 'existing code behaves differently' test.

for a senior

Should invoke Hyrum's Law by name or equivalent reasoning, discuss scoping the public API explicitly, and describe how their team catches these via changelog review plus automated tests rather than trusting classification alone.

for a principal

Should discuss how to design and communicate an API's compatibility boundary at organizational scale (internal/public package conventions, deprecation windows, behavioral changelogs) so Hyrum's Law risk is bounded rather than open-ended.

## What 'breaking' actually means SemVer defines a breaking change **functionally, not syntactically**: it's any change after which code that correctly used the previous public API can behave differently or stop working, without that code itself changing. This definition is deliberately broader than 'did we delete or rename an exported symbol,' because most real breakage in production doesn't come from deletions — deletions are loud and show up as compile errors. The dangerous breaking changes are the quiet ones: the symbol is still there, the signature still type-checks, but the behavior underneath shifted. ## The categories that remove nothing Concretely, a handful of change categories count as breaking even though nothing was removed. - **Changing a default parameter value** means every caller who didn't explicitly pass that argument silently gets new behavior — their code compiles and runs, it just does something different. - **Tightening input validation** (rejecting a value that used to be silently accepted or coerced) turns previously-working calls into runtime errors. - **Changing the shape or type of a return value** — adding a required field to a response object, changing an enum's set of values, or changing error types thrown — breaks any caller that destructures, serializes, or pattern-matches on the old shape. - **Changing timing/ordering guarantees** (e.g., a function that used to run synchronously now returns a `Promise`, or a collection that used to iterate in insertion order now doesn't) breaks callers relying on that behavior even if it was never explicitly documented. - **Relaxing an invariant** a caller depended on can silently degrade correctness without any error at all. ## Why the promise is scoped this broadly The reason SemVer scopes the promise this broadly traces back to what the version number exists to guarantee: **safe, unattended upgrade**. If 'breaking' only meant deletions, a resolver could bump you across a change that compiles fine but silently corrupts data or throws in production — worse than a compile error, because it's discovered later and further from the change. By defining breakage behaviorally, SemVer forces maintainers to reason about their entire observable contract, not just their type signatures. ## Hyrum's Law This is where **Hyrum's Law** becomes central: 'with a sufficient number of users of an API, it does not matter what you promise in the contract - all observable behaviors of your system will be depended on by somebody.' Even things the maintainer never intended to promise — the exact wording of an error message, the iteration order of an unordered collection, undocumented side effects — become de facto API surface once enough consumers exist. A theoretically 'correct' bug fix that changes one of these can still break real users, which is why disciplined maintainers publish changelogs describing observable behavior changes, not just signature diffs. ## The trade-off The trade-off is between **API stability** and **the ability to fix design mistakes**. A library that treats every observable behavior as permanently frozen accumulates cruft and can't improve; a library that classifies changes too loosely as 'non-breaking' burns consumer trust and turns every upgrade into a gamble. Mature projects resolve this with an explicit, scoped public API definition — documenting what is and isn't covered by the compatibility promise (e.g., internal packages in Go modules, `@internal` tags, or a documented 'unstable APIs' list) — so changes outside that boundary don't require a MAJOR bump even if some user happened to depend on them. ## Failure modes Failure modes in production typically look like a support ticket or a CI failure after a routine dependency bump: an application's tests pass locally against the old dependency version, get upgraded automatically via a MINOR/PATCH range, and then either throw at runtime (validation tightened) or — worse — silently produce wrong output (default value or return shape changed) with no test catching it because the suite didn't cover that specific default. ## Where it shows up A concrete real-world example: Node.js's own core APIs are notoriously conservative about this exact issue — changing the default encoding of a stream, or making an implicit type coercion stricter, has historically been treated as a semver-major change specifically because of the sheer number of undocumented behavioral dependencies across the ecosystem, even when the 'fix' made the API more correct. Express.js and similar frameworks maintain explicit migration guides between major versions precisely because their 'breaking changes' lists routinely include default-value and validation changes, not just removed methods.

  • Is fixing a security vulnerability that requires tightening input validation exempt from being a breaking change?
    No - functionally it's still breaking if it rejects input that used to be accepted, even though the motivation is good. Many projects still ship it as MAJOR, or as PATCH with an explicit documented security exception, because consumers need to know their calls might now fail; some ecosystems use separate security-advisory channels since the version number alone doesn't convey urgency.
  • How does 'internal' vs 'public' API scoping change what counts as breaking?
    SemVer's promise only covers the documented public surface, so changing something explicitly marked internal is not a SemVer-breaking change even if some consumer reached in and used it anyway - the maintainer isn't obligated to protect undocumented usage, only documented usage.
  • If a library changes an error message's exact text in a PATCH release, is that breaking?
    Usually not under the spec's intent, since message text typically isn't part of the documented contract - but per Hyrum's Law, if enough consumers pattern-match on that string, it breaks them in practice anyway, which is why some teams document error codes/types as stable and treat message text as unstable.

It's like a restaurant changing a recipe's salt level without changing the dish's name on the menu - nothing on the menu 'broke,' but a regular customer who orders the same dish gets a different meal than they expected.

saying these in an interview costs you the question

  • Thinks only deleting/renaming an exported symbol counts as breaking
  • Doesn't recognize a changed default value as breaking
  • Assumes tightened input validation is always fine to ship as PATCH
  • Can't explain Hyrum's Law or an equivalent 'undocumented behavior becomes API' idea
  • Believes the type-checker/compiler passing means the change isn't breaking

context