Skip to content

← Modules, Visibility, Testing and Docs step 17 of 22

Hard Framework

SemVer for Rust APIs: what actually breaks

This is the item that turns “I can write Rust” into “I can maintain a Rust library”, and it is where the visibility rules from 10.8 pay off. The public API is the reachable-path set, and a version bump is a claim about how that set changed.

pub fn classify(before: Vec<String>, after: Vec<String>) -> String

Return "major", "minor" or "patch" — the most severe applicable.

The API description language

Each line is one fact about the public surface.

struct Name exhaustive | non_exhaustive
enum   Name exhaustive | non_exhaustive
field  Struct.name  pub | private
variant Enum.Name
trait  Name
method Trait.name  required | defaulted
fn     name  Bound+Bound   (or `-` for no bounds)

Compare entities, not lines: struct S exhaustive becoming struct S non_exhaustive is one entity changing, not a removal plus an addition.

The table

Anything removed is major. A type, a field, a variant, a trait, a method, a function — if a path that used to work stops working, somebody’s build breaks.

Additions depend entirely on what was added.

change verdict why
add a field to a struct whose fields are all public major breaks every struct literal and every exhaustive pattern
add a field to a struct that already had a private field minor nobody could build it with a literal anyway
add a field to a #[non_exhaustive] struct minor that is what the attribute is for
add a variant to a plain enum major breaks every exhaustive match
add a variant to a #[non_exhaustive] enum minor downstream matches already need a wildcard
add a required trait method major every implementor stops compiling
add a defaulted trait method minor implementors inherit a body
tighten a bound (CloneClone + Send) major callers whose type is not Send are locked out
loosen a bound minor strictly more callers
add a new type, trait or function minor
add #[non_exhaustive] to an existing type major it breaks exactly what it exists to prevent later
remove #[non_exhaustive] minor only relaxes
field pubprivate major a readable field vanishes
field privatepub minor
nothing in the public surface changed patch

The first two rows are the pair worth remembering. The first private field in a struct is the breaking change, because it is the one that takes away literal construction. Every one after that is free.

Genuinely ambiguous cases, worth knowing about

These do not appear in the tests, because there is no correct answer to grade — but they are exactly the ones that will bite you.

Adding a defaulted trait method is listed as minor above and is what the reference says, but it can still break somebody: if a downstream type has an inherent method of the same name and the trait is in scope, the call becomes ambiguous. That is the E0659 machinery from 10.6 resurfacing as a compatibility hazard.

Raising your MSRV is treated as a minor bump by convention and breaks real users on older toolchains. The community does not agree about this. The current compromise is the rust-version field plus the MSRV-aware resolver (resolver 3, implied by edition 2024), which will at least pick an older dependency version rather than fail.

Auto-traits change silently. Adding an Rc to a private field can make your type stop being Send, and every downstream spawn that moved it stops compiling. There is no syntax to point at in the diff. cargo-semver-checks is a very good mechanical guard and it cannot catch this, nor behavioural changes, nor Drop semantics.

And one plain fact: #[non_exhaustive] has no effect within the defining crate. In a single file — like every submission on this site — you can still match a #[non_exhaustive] enum exhaustively with no wildcard and no warning. The attribute is a promise to other crates.