Warning

"dead_code" — How to Suppress Unused Code Warnings in Rust

Suppress Rust dead_code warnings by adding the #[allow(dead_code)] attribute to the specific item or the entire crate.

The compiler found code you never call

You write a helper function to parse a configuration file. You test it. It works. Then you realize the standard library has a cleaner approach, or you refactor the logic inline. You leave the old function in the file, thinking you might need it later. You run cargo build. The compiler stops you with a warning about dead code.

You are tempted to silence the warning. Before you paste an attribute to suppress it, understand what the compiler is telling you. Rust treats dead code as a signal, not just noise. Dead code increases compilation time, clutters the mental model of the codebase, and often hides bugs or outdated assumptions. The warning exists to keep your project lean and honest.

What dead code actually means

Dead code is any item that the compiler can prove is never reachable. This includes functions, structs, enums, impl blocks, and even fields inside structs. The compiler builds a graph of all calls and references in your crate. If an item is private and has no path from a public entry point or main, the compiler marks it as dead.

Public items are never flagged as dead code. If you mark a function pub, it becomes part of your crate's API surface. The compiler assumes external code might call it, so it stays silent. This distinction is crucial. Making something public just to silence a warning is a mistake. You are exposing internal implementation details that callers shouldn't see, which breaks encapsulation and makes future refactoring harder.

Dead code warnings also apply to struct fields. If you define a struct with a field that is never read, the compiler warns you. This catches typos in field names and prevents structs from carrying unnecessary data.

The underscore convention

The community standard for intentional dead code is the underscore prefix. If you need a function or variable that won't be called immediately, or if you are scaffolding code, name it with a leading underscore. The compiler treats identifiers starting with _ as intentionally unused and suppresses the warning automatically.

// This function is dead code, but the underscore prefix suppresses the warning.
// Use this for placeholders, FFI targets, or code you plan to wire up later.
fn _parse_config_legacy() {
    // Implementation here
}

fn main() {
    // _parse_config_legacy is never called.
    // No warning appears.
}

This convention is preferred over attributes because it keeps the suppression attached to the name, not the metadata. It signals to other developers that the dead code is intentional. If you see _helper, you know someone decided to keep it. If you see a bare helper with a warning, you know it's likely a mistake.

Use the underscore prefix for any dead code that exists by design. It is the cleanest way to acknowledge the compiler's concern without adding noise.

Suppressing with attributes

When the underscore prefix isn't an option, use the #[allow(dead_code)] attribute. This attribute tells the compiler to ignore the dead code warning for a specific item. Attributes apply to the item immediately following them. You can place them on functions, structs, modules, or fields.

// Suppress the warning for this specific struct.
// Useful when the struct is used only by macros or external tools.
#[allow(dead_code)]
struct InternalState {
    // This field is never read in Rust code.
    // It might be used by a C library via FFI, or reserved for future use.
    reserved_token: u64,
}

fn main() {
    let state = InternalState { reserved_token: 0 };
    // No warning about reserved_token being unused.
}

Attributes have scope. #[allow(dead_code)] on a function only affects that function. #[allow(dead_code)] on a module affects everything inside the module. Be careful with module-level suppression. It hides warnings for all dead code within the module, including accidental dead code. This defeats the purpose of the lint and makes it harder to spot real issues.

Keep suppression as local as possible. If you suppress a warning on a module, you are trusting that every item inside is dead by design. That trust is rarely justified.

Realistic scenarios

Dead code warnings appear in specific patterns. Understanding these patterns helps you decide whether to delete, suppress, or refactor.

FFI and external callers

When you write code for C or another language to call, Rust might not see the call site. If you export a function with #[no_mangle] but keep it private, Rust warns that the function is dead. The C code will call it, but Rust doesn't know that.

// This function is called by C code, not Rust.
// Rust sees no callers and warns about dead code.
#[no_mangle]
fn start_engine() {
    // Engine logic
}

fn main() {
    // start_engine is never called here.
    // Warning: function `start_engine` is never used.
}

The fix is to make the function public. Public items are part of the API and never trigger dead code warnings. If you don't want to expose it to other Rust code, use pub(crate) or keep it public and rely on documentation to indicate it's for FFI only. The #[no_mangle] attribute controls the symbol name for the linker, not Rust's visibility.

Make FFI targets public to satisfy the compiler. The visibility controls Rust's reachability analysis, not the linker's behavior.

Struct fields for serialization or FFI

Sometimes a struct has fields that are never read by Rust code but are required for a binary layout or a serialization format. For example, a struct representing a hardware register might have reserved bits. Or a struct used by serde might have fields that are only written, never read.

// This struct maps to a hardware register layout.
// The `reserved` field must exist for alignment, but Rust never reads it.
#[allow(dead_code)]
struct HardwareRegister {
    command: u8,
    status: u8,
    // This field is padding. It is never accessed.
    reserved: u16,
}

fn main() {
    let reg = HardwareRegister {
        command: 0x01,
        status: 0x00,
        reserved: 0,
    };
    // No warning about reserved.
}

Here, #[allow(dead_code)] on the struct is appropriate. The underscore prefix doesn't work well for struct fields when the field name must match an external specification. Suppressing the warning on the struct is the pragmatic choice.

Conditional compilation

Code guarded by #[cfg(...)] attributes is handled intelligently. If a feature flag is disabled, the compiler ignores the code entirely. It does not warn about dead code inside disabled cfg blocks. This allows you to keep platform-specific or feature-gated code without warnings.

However, if you have a cfg block that is always enabled but the code inside is dead, the warning still applies. The cfg only suppresses warnings when the code is actually excluded from the build.

Pitfalls and scope

The most common mistake is suppressing dead code warnings at the crate level. Adding #![allow(dead_code)] to the top of main.rs or lib.rs disables the warning for the entire project. This is dangerous. It hides accidental dead code, makes refactoring harder, and encourages code accumulation.

Crate-level suppression is a debt you pay with interest in bugs. When you return to the code months later, you won't remember which functions are dead by design and which are forgotten leftovers. The compiler's warning is a free audit. Turning it off removes that safety net.

Module-level suppression is also risky. If you put #[allow(dead_code)] on a module, every dead item inside is silenced. This includes helper functions you forgot to delete and structs you replaced. Use module-level suppression only in generated code or in modules where every item is intentionally dead, such as a stub module for testing.

Another pitfall is confusing dead code with unused variables. The dead_code lint applies to definitions. Unused variables trigger a different warning. If you have a variable that is assigned but never read, the compiler warns about unused_variables. The fix there is often the underscore prefix on the variable name, like let _result = compute();. This is distinct from dead_code, which targets functions and types.

Decision matrix

Choose the right approach based on why the code is dead. Each scenario has a standard solution.

Delete the code when it is truly obsolete. If you refactored and the old function is no longer needed, remove it. Version control preserves history. You can always recover deleted code from git. Keeping dead code in the source tree adds noise and maintenance burden.

Prefix with underscore when the code is a placeholder or intentional stub. Use _helper for functions you plan to wire up later. Use _field for struct fields that must exist but aren't accessed. This signals intent to readers and satisfies the compiler without attributes.

Use #[allow(dead_code)] when the item must have a specific name or structure that conflicts with the underscore convention. Apply this to FFI structs with reserved fields, or to items required by macros that generate dead code. Keep the attribute on the specific item, not the parent module.

Make the item pub when it is part of the API surface. If external code calls the function, or if it is an FFI target, mark it public. Public items are never flagged as dead code. Use pub(crate) if you want to limit visibility to the crate while suppressing the warning.

Reach for #![allow(dead_code)] only in throwaway scratch files or benchmarks where you are experimenting with multiple approaches. Never use crate-level suppression in shared code or production libraries. It hides mistakes and degrades code quality.

Treat dead code warnings as a gift. The compiler is pointing out dead weight. Remove it, suppress it intentionally, or expose it as API. Don't ignore it.

Where to go next