Stable Language Evolution

Compatibility and Deprecation

As a library evolves across minor releases, you often want to replace an older function name, configuration struct, or enum variant with a better design without breaking downstream callers. Rust provides the built-in #[deprecated] attribute to guide users to the new API with actionable compiler warnings while keeping the old API compiling cleanly until the next major SemVer release.

#[deprecated(since = "0.4.0", note = "use `Client::connect_with_timeout` instead")]
pub fn connect_legacy(...) { ... }
        |
        +-- Existing code still compiles without errors! (100% SemVer compatible)
        +-- Compiler & IDE emit a targeted `warning: use of deprecated function...`
            pointing callers directly to `Client::connect_with_timeout`!

Using the #[deprecated] Attribute

The #[deprecated] attribute can be attached to functions, methods, structs, struct fields, enums, enum variants, traits, type aliases, constants, macros, and modules:

Attribute Form Compiler Diagnostic Produced
#[deprecated] warning: use of deprecated function foo
#[deprecated = "use bar instead"] warning: use of deprecated function foo: use bar instead
#[deprecated(since = "1.2.0", note = "use bar instead")] (Recommended) Includes both the exact version when deprecation began and the migration instruction!
use std::time::Duration;

#[derive(Debug, PartialEq, Eq)]
pub struct Connection {
    pub endpoint: String,
    pub timeout: Duration,
}

impl Connection {
    /// Connects to `endpoint` with an explicit [`Duration`] timeout.
    pub fn connect(endpoint: impl Into<String>, timeout: Duration) -> Self {
        Self {
            endpoint: endpoint.into(),
            timeout,
        }
    }

    /// Connects to `endpoint` with a timeout in milliseconds.
    #[deprecated(
        since = "0.2.0",
        note = "pass a typed `std::time::Duration` to `Connection::connect` instead"
    )]
    pub fn connect_ms(endpoint: &str, timeout_ms: u64) -> Self {
        // Delegate the deprecated method directly to the new canonical implementation!
        Self::connect(endpoint, Duration::from_millis(timeout_ms))
    }
}

#[expect(
    deprecated,
    reason = "verifying backward-compatible behavior of deprecated connect_ms wrapper"
)]
fn test_legacy_caller() -> Connection {
    Connection::connect_ms("127.0.0.1:8080", 500)
}

fn main() {
    let modern = Connection::connect("127.0.0.1:8080", Duration::from_millis(500));
    let legacy = test_legacy_caller();

    assert_eq!(modern, legacy);
    println!("Modern and deprecated wrapper produced identical Connection: {:?}", modern.timeout);
}
Modern and deprecated wrapper produced identical Connection: 500ms

Non-Breaking Evolution Patterns in Libraries

  1. Delegate deprecated functions to the new implementation: Instead of duplicating logic inside a #[deprecated] function, make its body a 1-line call to the new function and suppress internal warnings in its unit test with #[expect(deprecated, reason = "...")].
  2. Rename types with a deprecated pub type alias: If you rename pub struct OldName to pub struct NewName, keep a deprecated type alias #[deprecated(since = "...", note = "renamed to NewName")] pub type OldName = NewName; so existing callers continue to compile!
  3. Re-export moved items with a deprecated pub use: If you move an item to another module, leave a #[deprecated(note = "moved to crate::new_mod::Item")] pub use crate::new_mod::Item; in the old module.