Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

How Rust Knows When You're Not Using a Variable: The _ Prefix Trick

by Ugochukwu Chizaram Omumusinachi

.

Updated Mon Jun 02 2025

.
How Rust Knows When You're Not Using a Variable: The _ Prefix Trick

In no more than 5 minutes we'll show you the difference between an empty binding and a named silenced binding and when to use them.

In Rust, aside from variables and struct fields, you can also bind memory to patterns. Some of these patterns are used by the compiler to signal certain conditions, like when a variable will never be used or when it may be used. This is the point of this article: how does the Rust compiler know you'll use a variable?

It all boils down to the _ and _variable_name patterns, which we call empty binding and named silenced bindings respectively. With these two patterns, Rust can know if you intend on using a variable even though you haven't used it yet (in your current code structure) or you don't have any expected usage of that variable.

The Two Underscore Patterns: _ vs _name

Although these two patterns seem alike, as their aliases imply, they're not the same. Let's look at some of the purposes and differences between both.

_: Empty Binding

This type of binding signals to the compiler that this value won't be used further in your code, which means it can be ignored or discarded.

This binding can be used anywhere a variable can be used: structure destructuring, tuple bindings, scoped bindings, function return bindings, and more.

Here's an example to help you understand it better:


rust
fn main() {
    let _ = "Ignored very big string";
    // println!("Very big string: {}", _);
    // The above code won't run as _ cannot be printed since it binds to nothing
}

Since the _ wildcard pattern doesn't create a binding, it can be used multiple times in the same pattern, including in tuple destructuring or as a catch-all for the rest of the values in a slice.


rust
fn main() {
    let tuple = (1, 2, 3, 4);

    // You can use `_` multiple times — each one discards the value at that position
    let (_, second, _, fourth) = tuple;
    println!("second: {}, fourth: {}", second, fourth);

    let array = [10, 20, 30, 40, 50];

    // Match the first two elements, ignore the rest with `..` and use `_` for positions you don't care about
    // Notice how we can bind two values with _ at the same time
    // Try changing this to any other variable name and running the code
    match array {
        [first, _, _, ..] => {
            println!("first: {}", first);
        }
    }

    // Use `_` in multiple match arms — always matches, doesn't bind
    let value = Some(42);
    match value {
        Some(_) => println!("Got a value (but not binding it)"),
        None => println!("No value"),
    }
}
_named_silenced Binding

In this binding, the value is stored just like a normal variable and can be assigned and used like a normal variable. It only signals to Rust that this variable may not be used, which is important for linting and some slight performance optimizations.

Since this is a valid variable and not a keyword pattern like _ , you can use it just like any variable.

Look at this code sample, showing how it can be used just like a normal variable:


rust
// Notice how it can also be used in function signatures, signaling that the variable may not be used
fn print_value(_val: i32) {
    // Even though, as you can see here, it is being used
    println!("Value: {}", _val);
}

fn main() {
    // `_used_var` is a regular binding — you can use it like any other variable
    let mut _used_var = 10;

    // You can perform operations on it
    _used_var += 5;

    // You can pass it to functions
    print_value(_used_var);

    // You can reassign it
    _used_var = 42;

    // Compiler is totally fine with this — it's a normal variable
    println!("Final _used_var: {}", _used_var);

    // `_not_used` is bound but never used — no compiler warning
    // Because of the leading underscore, Rust assumes this is intentional
    let _not_used = expensive_computation();

    // `normal_unused` would trigger a warning, because it's unused and has no underscore
    // let normal_unused = 123; // <-- Uncommenting this will cause a warning!
}

fn expensive_computation() -> i32 {
    println!("Running expensive computation...");
    99
}

In context: the _ binding will be completely ignored while the _named_variable binding won't be ignored, but signals to the compiler that this variable may not be used.

When—and When Not—to Use These Patterns

Underscore tricks are powerful but should be applied thoughtfully. Here's when to use each pattern:

Use _ When You Truly Don't Need the Value

The empty binding works perfectly for placeholder loops, partial destructures, and situations where you need to consume a value but don't care about its contents:


    rust
    fn main() {
        // Placeholder in loops
        for _ in 0..5 {
            println!("Hello!");
        }
        
        // Partial destructuring
        let (important_value, _) = get_coordinates();
        println!("X coordinate: {}", important_value);
        
        // Consuming Result when you only care about success/failure
        let _ = risky_operation().map_err(|e| eprintln!("Error: {}", e));
    }
    
    fn get_coordinates() -> (i32, i32) {
        (10, 20)
    }
    
    fn risky_operation() -> Result<String, &'static str> {
        Ok("Success".to_string())
    }

Use `_name` for Self-Documenting Code
Named silenced bindings work best when you want descriptive variable names but don't plan to reference them immediately:

    rust
    fn process_user_data(user: User) {
        let _user_id = user.id; // May be used for logging later
        let _creation_timestamp = user.created_at; // Reserved for audit features
        
        // Current logic only processes the name
        println!("Processing user: {}", user.name);
    }
    struct User {
        id: u32,
        name: String,
        created_at: String,
    }

Avoid Silencing Variables You May Actually Use Later

Don't use underscore prefixes to silence warnings for variables you genuinely plan to use. Silent discards can mask real bugs and make code maintenance harder:


rust
// Don't do this
fn bad_example() {
    let _important_data = fetch_critical_info();
    // ... lots of code later ...
    // Oops! Forgot to use _important_data, and no warning to remind us
}

// Do this instead
fn good_example() {
    let important_data = fetch_critical_info();
    // Compiler will warn if we forget to use it
    process_data(important_data);
}

fn fetch_critical_info() -> String {
    "Critical data".to_string()
}

fn process_data(_data: String) {
    // Processing logic here
}

Remember: _ Still Evaluates Expressions

One important detail: plain _ still evaluates the expression, so side effects happen. You just can't refer back to the value:


rust
fn main() {
    let _ = expensive_computation(); // This still runs!
    // But you can't access the result
}

fn expensive_computation() -> i32 {
    println!("Computing..."); // This will print
    42
}

Summary

Understanding Rust's underscore patterns helps you write cleaner, more intentional code while working effectively with the compiler's warning system.

The key distinctions are:


_ = discard entirely, no warning, no binding created
_name = keep a named binding, but silence "unused" warnings

Use _ when you truly don't need a value, and use _name when you want descriptive, self-documenting variable names that may not be immediately used.

For more advanced warning control and linting options, explore Clippy's lint settings. The Rust Book's pattern-matching chapter provides deeper dives into these concepts and other powerful pattern-matching techniques in Rust.

Course image
Become a Rust Backend Engineeer today

All-in-one Rust course for learning backend engineering with Rust. This comprehensive course is designed for Rust developers seeking proficiency in Rust.

Start Learning Now

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Backend Tips, Every week

Backend Tips, Every week