🎓 A1Learner
🔍
0 of 149 lessons completed

Rust Attributes

An attribute is an instruction written above an item for the compiler or a tool. The #[derive(Debug)] line above a struct is an attribute.

An attribute sits above its item

The attribute is the line in brackets. It gives an instruction about the item directly below it:

#[derive(Debug)]
struct Point { x: i32, y: i32 }

deprecated warns the callers

Mark a function #[deprecated] and every caller gets a warning naming your replacement. The call still runs — it prints 1 — but the compiler complains:

#[deprecated = "use new_total instead"]
fn total() -> i32 { 1 }
fn main() { println!("{}", total()); }

must_use: a return value that must not be dropped

Mark a function #[must_use], and the compiler warns any caller who throws its result away. Result works exactly this way, which is why ignoring a Result warns.

#[must_use = "the answer only exists in the return value"]
fn answer() -> i32 { 42 }

fn main() {
    let n = answer();
    println!("n = {n}");
}

Lint levels, and expect

allow hides a warning. warn shows it. deny turns it into an error. forbid makes it an error no later attribute can soften. expect works like allow. But it warns you if the expected warning never appears. So a silence that is no longer needed gets cleaned up. It does not live forever.

fn main() {
    #[allow(unused_variables)]
    let spare = 5;

    #[expect(unused_variables)]
    let ignored = 6;

    println!("done");
}

Feature gates need nightly

An unfinished feature is switched on with #![feature(...)] at the very top of a crate. On stable Rust the compiler refuses:

#![feature(async_closure)]
fn main() {}
// error E0554: #![feature] may not be used on the stable release channel

Mark the type

Write #[non_exhaustive] above the item. Rust std uses this itself — io::ErrorKind is the famous example.

#[non_exhaustive]
pub enum AppError {
    NotFound,
    PermissionDenied,
}

Other crates must add a catch-all arm

Code in another crate may still match on the enum. But the match needs help to compile. It only compiles with a _ arm. That arm sits at the end. Without it, the compiler refuses with "non-exhaustive patterns" — the very problem the attribute plans for.

match error {
    AppError::NotFound => "not found",
    AppError::PermissionDenied => "permission denied",
    _ => "some future error",
}

Structs cannot be built with a literal outside their crate

Other crates can read a public field, but they cannot write Config { ... } — the library may add a field later. So a public struct marked this way should ship a constructor function.

impl Config {
    pub fn new(retries: u32, verbose: bool) -> Config {
        Config { retries, verbose }
    }
}

Patterns on the struct need ..

Matching a non-exhaustive struct in another crate must end the pattern with .., which says: there may be fields I am not naming. Here config came from the library, holding retries = 3.

let Config { retries, .. } = config;
println!("retries = {retries}");

Inside its own crate, nothing changes

In this example, both types carry #[non_exhaustive]. Yet everything in main works the plain way. The struct is built with a literal. It is split with a pattern. The pattern names every field. The match lists both variants. It has no catch-all arm. That is the first lesson of this attribute: inside the crate that defines the type, it changes nothing at all. The restrictions switch on only for code in other crates. Their matches will need the _ arm, their patterns will need .., and they cannot build Config with a literal. This crate may grow next year. It may gain a variant or a field. Their code still compiles then.

#[non_exhaustive]
pub enum AppError {
    NotFound,
    PermissionDenied,
}

#[non_exhaustive]
pub struct Config {
    pub retries: u32,
    pub verbose: bool,
}

fn describe(error: &AppError) -> &'static str {
    match error {
        AppError::NotFound => "not found",
        AppError::PermissionDenied => "permission denied",
    }
}

fn main() {
    let config = Config { retries: 3, verbose: true };
    let Config { retries, verbose } = config;
    println!("retries = {retries}, verbose = {verbose}");
    println!("error = {}", describe(&AppError::NotFound));
}

Example

#[derive(Debug, Clone)]
struct Book { title: String, pages: u32 }
fn main() {
    let b1 = Book { title: "Rust in Action".to_string(), pages: 320 };
    let b2 = b1.clone();
    println!("{:?}", b1);
    println!("copy: {:?}", b2);
}

Key points

  • An attribute gives extra information or instructions to the compiler or a tool — it is not code that runs
  • Use #[cfg(...)] to include code only in some builds: #[cfg(test)] includes code only while testing
  • An inner attribute starts with #![ ... ] and sits at the very top of a file.
  • It applies to the whole file or crate.
  • It does not apply to one item.
  • Crates can bring their own attributes.
  • In #[tokio::main], tokio is the crate and main is its attribute.
  • It rewrites your async main so a runtime starts first.
  • Attributes take arguments in three shapes.
  • A plain value: #[attribute(value)].
  • A name and value: #[attribute(key = "value")].
  • Or a whole assignment in an inner attribute like #![crate_name = "rary"].
  • One attribute may list several values at once.
  • Delete the attribute line, and that item loses the instruction — the struct stays, but the free Debug printing goes away.
  • Use #[non_exhaustive] on a public enum, struct, or single enum variant whose shape you may extend in a later version
  • Inside the crate that defines the type, the attribute has no effect at all
  • Outside that crate: matches on the enum need a _ arm, struct patterns need .., and struct literals cannot build the type
  • An enum can still be constructed outside its crate — only matching on it and building structs are restricted
  • With it, users write the catch-all arm on day one, so your next release cannot break them.

In this example, the attribute #[derive(Debug, Clone)] sits above the Book struct and gives two instructions. Debug lets {:?} print the whole book, fields and all. Clone adds a .clone() method, so b2 becomes a full copy of b1. main prints both, and the lines are identical: Book { title: "Rust in Action", pages: 320 }. Delete the attribute line and both powers disappear — the struct itself would not change at all.

Note

An attribute changes the thing directly below it.

Without this attribute, adding one variant to a published enum breaks every downstream match.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What is an attribute in Rust?