Semantic Versioning
Rust and Cargo follow Semantic Versioning 2.0.0 (SemVer) (MAJOR.MINOR.PATCH), encoding API compatibility guarantees directly into version numbers so cargo update can upgrade dependencies safely without breaking downstream code.
Version Format: 2 . 4 . 1
| | |
| | +-- PATCH: Backward-compatible bug fixes & internal perf
| +------ MINOR: Backward-compatible new features / APIs
+---------- MAJOR: Breaking API changes (or 0.MINOR.PATCH pre-1.0)
What Counts as a Breaking vs Non-Breaking Change in Rust?
Following the official Cargo SemVer reference, several seemingly small code edits in Rust are breaking changes (requiring a MAJOR version bump on >=1.0.0, or a MINOR bump on 0.y.z):
| Change to Public API | Breaking or Non-Breaking? | Why & How to Make It Non-Breaking |
|---|---|---|
Adding a new pub fn or pub struct |
Non-Breaking (Minor) | Existing callers are unaffected. |
Adding a field to a pub struct with all pub fields |
Breaking (Major) | Breaks struct literal construction Point { x, y } and exhaustive pattern matching let Point { x, y } = p. Mark the struct #[non_exhaustive] upfront to allow adding fields later! |
Adding a variant to a pub enum |
Breaking (Major) | Breaks exhaustive match expressions in downstream crates. Mark the enum #[non_exhaustive] upfront to require a _ => wildcard arm in external callers! |
Adding a method to a pub trait without a default body |
Breaking (Major) | Breaks downstream impl MyTrait for CustomType blocks. Provide a default method implementation (fn new_method(&self) { ... }) to make it non-breaking. |
| Tightening generic trait bounds on a function | Breaking (Major) | Callers passing types that don't satisfy the new bound fail to compile (relaxing bounds is non-breaking). |
Removing a Copy, Clone, Send, or Sync impl |
Breaking (Major) | Downstream code relying on auto-traits or Clone stops compiling. |
Future-Proofing Structs and Enums with #[non_exhaustive]
Applying #[non_exhaustive] to a public struct, enum, or enum variant allows your crate to add new fields or variants in future minor releases (1.1.0 -> 1.2.0) without breaking downstream callers:
/// Configuration options that may grow new fields in future minor versions.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub timeout_ms: u64,
pub max_retries: u32,
}
impl Default for ClientOptions {
fn default() -> Self {
Self {
timeout_ms: 5_000,
max_retries: 3,
}
}
}
/// Server error codes that may gain new variants in future minor versions.
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum ApiErrorCode {
RateLimited,
Unauthorized,
InternalServerError,
}
fn describe_error(code: ApiErrorCode) -> &'static str {
// External crates MUST include a `_ =>` wildcard arm when matching a `#[non_exhaustive]` enum:
match code {
ApiErrorCode::RateLimited => "429 Too Many Requests",
ApiErrorCode::Unauthorized => "401 Unauthorized",
_ => "Other API Error",
}
}
fn main() {
let mut opts = ClientOptions::default();
opts.timeout_ms = 2_500;
println!("Options: {opts:?}");
println!("Error: {}", describe_error(ApiErrorCode::RateLimited));
}
Options: ClientOptions { timeout_ms: 2500, max_retries: 3 }
Error: 429 Too Many Requests
Automated SemVer Checking with cargo-semver-checks
Before publishing a new minor or patch release to crates.io, run cargo-semver-checks (cargo install cargo-semver-checks --locked). It extracts the rustdoc JSON of your current workspace and compares it against the previous published baseline on crates.io (or a Git tag) across hundreds of lint rules to catch accidental breaking changes automatically:
cargo semver-checks check-release