Which changes to traits, impls and public types break downstream code, following the Cargo book's SemVer compatibility chapter and RFC 1105 (API evolution). "Major" means a new major version (or a new minor version below 1.0); "possibly breaking" means it can break some callers in practice, usually through name collisions or type inference, and is conventionally allowed in a minor release.
Contents
- Trait changes
- Impl changes
- Generic bounds and parameters
- Public types
- Auto traits leak
- Designing for evolution
- Checking before release
Trait changes
| Change | Impact | Why |
|---|---|---|
| Add a method without a default | major | every downstream implementor fails to compile |
| Add a method with a default | possibly breaking | calls may become ambiguous with a same-named method from another trait in scope |
| Add a method that makes the trait dyn-incompatible | major | dyn Trait users fail (E0038); add where Self: Sized to keep it out of the vtable |
| Change a method signature (params, return, receiver, generics) | major | implementors and callers both break |
| Add an associated type | major | implementors must define it (associated type defaults are unstable) |
| Add an associated const without a default | major | implementors must define it |
Add a defaulted type parameter to the trait (trait T<X = ()>) | minor | existing uses keep the default |
| Add a supertrait | major | implementors may not implement the new supertrait |
| Remove a supertrait | major | callers relying on supertrait methods through the bound break |
| Any change to a sealed trait's items | minor (for items callers do not call) | nobody outside the crate implements it |
Seal traits that users only consume, so you can add required methods later. Leave traits open only when downstream implementations are the point (plugins, backends, codecs).
Impl changes
| Change | Impact | Notes |
|---|---|---|
| Remove a trait impl | major | callers using the impl break |
| Add an impl of an existing trait for an existing type | minor, possibly breaking | can break inference: adding impl From<u16> for Id makes x.into() ambiguous where it was not |
Add a blanket impl (impl<T: Bound> Trait for T) of an existing trait | major | can overlap with downstream impls (E0119 in their code) |
| Add inherent methods | possibly breaking | an inherent method shadows a trait method of the same name callers were using |
| Implement a foreign trait for a local type | minor | coherence keeps it yours |
Generic bounds and parameters
| Change | Impact |
|---|---|
Tighten a bound (T: Clone -> T: Clone + Send) on a fn, type or impl | major |
| Loosen a bound | minor |
| Add a type parameter with a default to a type | minor |
| Add a type parameter to a fn | possibly breaking (callers using turbofish break) |
Replace a concrete parameter type with a generic that still accepts it (&str -> impl AsRef<str>) | minor, possibly breaking (inference) |
Change impl Trait in return position to another concrete type | minor (opaque), unless auto traits change (below) |
Public types
| Change | Impact |
|---|---|
Add a variant to an exhaustive pub enum | major (exhaustive matches break) |
Add a variant to a #[non_exhaustive] enum | minor |
Add #[non_exhaustive] to an existing enum/struct | major |
| Add a public field to a struct with only public fields | major (struct literals and patterns break) |
| Add a private field when one already exists | minor |
Add a field to a #[non_exhaustive] struct | minor |
Remove a derive (Debug, Clone, Hash...) | major |
| Add a derive | minor |
Change #[repr] or layout of a repr(C) type | major |
#[non_exhaustive] on public error enums and config structs (built through constructors or builders) keeps
future additions minor. Callers must then write _ => arms and cannot use struct literals.
Auto traits leak
Send, Sync, Unpin, UnwindSafe and RefUnwindSafe are inferred from fields and from the concrete type
behind impl Trait. They are part of your public API even though you never wrote them:
- Adding an
Rc,RefCell, raw pointer or!Sendguard field to a public struct removesSend/Sync: major. - Changing what an
-> impl Futurereturns so it holds a!Sendvalue across an.awaitremovesSendfrom the opaque type: major for callers who spawned it. - Guard against accidents with compile-time assertions (see testing-trait-designs.md):
pub struct Client { inner: std::sync::Arc<std::sync::Mutex<Vec<u8>>> }
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Client>();
};Designing for evolution
- Seal consumer-only traits (
pub trait Codec: private::Sealed). - Give new trait methods defaults, and prefer names unlikely to collide.
- Put constructors and generic helpers behind
where Self: Sizedso the trait stays dyn-compatible as it grows. - Mark error enums and option structs
#[non_exhaustive]from the first release. - Return opaque types (
impl Iterator, newtypes) instead of concrete std/foreign types, so the implementation can change. - Keep foreign types out of public signatures when they have their own semver (a
reqwest::Errorin your API ties your major version to theirs); wrap them. - Implement
From(notInto) and keep the set ofFromimpls deliberate: each one is a commitment and a possible inference hazard.
Checking before release
cargo semver-checkscompares the public API against the last published version and reports many of the breaking changes above (removed items, new required trait items, enum variants, auto-trait changes).cargo public-api diff(nightly rustdoc JSON) shows the full public surface change.- Run downstream crates' test suites (crater-style) for widely used libraries.
- Write the change into
CHANGELOG.mdwith its classification.
rust-borrow-checker-error-explainer
Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.
rust-cargo-workspace-designer
Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool.
rust-error-handling-thiserror-anyhow-designer
Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.
rust-lifetime-annotation-advisor
Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.
rust-tokio-async-service-builder
Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.
rust-unsafe-block-auditor
Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.