Course contentsShow
← Rust
Lesson 1629 of 1,91014. System Integration and EcosystemFree lesson

Versioning and Breaking Changes

You will practice semver discipline to ensure public APIs remain stable for ecosystem consumers.

In this lesson, you will learn how to maintain semantic versioning (SemVer) discipline to ensure your public API remains stable for ecosystem consumers.

When you publish a crate, your version number is a contract with every developer who depends on your code. In the Rust ecosystem, SemVer is enforced by cargo, which assumes that 0.x.y versions may include breaking changes, while 1.x.y and above follow strict rules where major versions signify breaking changes.

Defining a "Breaking Change"

A breaking change is any modification that forces a downstream consumer to update their source code to compile against your new version. In Rust, this goes beyond just changing function signatures; it includes:

  1. Removing or renaming public items: Removing a pub struct, enum, or function.
  2. Changing trait bounds: Adding a trait bound to a generic function can break implementations that don't satisfy that new bound.
  3. Modifying Enum variants: Adding a variant to a non-#[non_exhaustive] enum breaks existing match statements that do not have a wildcard arm.
  4. Changing struct layouts: Since you are already comfortable with #[repr(C)] and FFI-safe layouts, remember that changing field visibility or reordering fields in a public struct breaks code that uses direct instantiation or pattern matching.

Maintaining API Stability

To avoid breaking your consumers, utilize the #[non_exhaustive] attribute on public enums and structs. This allows you to add new variants or fields in the future without triggering a breaking change, as it forces consumers to handle future-proofing in their match blocks.

Consider this transition:

// Version 1.0.0
pub struct Config {
    pub timeout: u64,
}

// Breaking Change: Changing the type, even if logically better
pub struct Config {
    pub timeout: std::time::Duration,
}

If you absolutely must change a signature, don't update the existing code in a way that breaks compatibility. Instead, keep the old item (perhaps marking it with #[deprecated]) and introduce a new, improved version alongside it. This allows your users to migrate at their own pace rather than forcing a build failure in their CI pipeline.

Key Takeaway: A breaking change is any API alteration that forces a downstream user to modify their code; always use deprecation cycles and #[non_exhaustive] to preserve the contract of your crate.