How to Use the Underscore _ Placeholder in Rust

The underscore _ placeholder in Rust ignores values to prevent unused variable warnings.

The wildcard that saves you from the compiler

You're writing a callback for a library function. The signature demands three arguments. You only need the first one. You delete the other two from your closure. The compiler rejects you with a mismatched types error because your closure doesn't match the trait. You add the arguments back. The compiler immediately warns you about unused variables. You're stuck between a type error and a warning.

The underscore _ breaks this trap. It tells the compiler, "I acknowledge this value exists, and I am intentionally discarding it." It accepts the argument, consumes the value, and vanishes without creating a variable. You get type safety without the noise.

What the underscore actually is

The underscore is a pattern, not a variable. In Rust, patterns are used to destructure values and bind names to parts of them. The underscore is the wildcard pattern. It matches any value and binds nothing.

Think of _ like a wildcard card in a game. It matches whatever you need it to match, but it has no value of its own. You can't play the wildcard as a specific card later. It just fulfills the requirement to make the move legal.

In Python or JavaScript, you might leave a variable unused and ignore the linter. Rust treats unused variables as a signal that you made a mistake. The compiler assumes you meant to use the value. The underscore is how you correct that assumption. It's an explicit declaration that the value is irrelevant to your logic.

Treat _ as a trash can, not a storage bin. The value goes in and gets dropped. You can't retrieve it.

Minimal example

Here's the underscore in its most common roles: ignoring a function argument, matching a default case, and discarding a binding.

fn main() {
    // This closure matches a trait that requires two arguments.
    // We only care about the first one.
    let callback = |x: i32, _: i32| {
        // `_` consumes the second argument but binds no name.
        // The value is dropped immediately.
        println!("Processing {}", x);
    };

    callback(10, 20);

    // Match any value that isn't 1.
    let status = 42;
    match status {
        1 => println!("Active"),
        // `_` catches everything else.
        _ => println!("Inactive"),
    }

    // Discard a value you don't need.
    let data = compute_heavy_value();
    let _ = data; // Explicitly drop data to silence warnings.
}

fn compute_heavy_value() -> String {
    String::from("result")
}

What happens under the hood

When you write let _ = value;, the compiler generates code to move value into the underscore. Since _ doesn't create a binding, the value is immediately eligible for dropping. If value implements Drop, the drop runs at the end of the scope where _ was declared.

This is the part that surprises newcomers. The underscore moves the value. If you have a non-Copy type, let _ = x; moves x. You can't use x afterward. The compiler will reject you with E0382 (use of moved value) if you try.

fn main() {
    let s = String::from("hello");

    // This moves s into the underscore.
    let _ = s;

    // Error: E0382. s was moved.
    // println!("{}", s);
}

The underscore doesn't skip the drop. It just skips the binding. The value is transferred to the hole and cleaned up when the hole goes out of scope. This matters for resources like file handles or locks. If you ignore a lock with _, the lock is released at the end of the scope, not immediately.

use std::sync::Mutex;

fn main() {
    let mutex = Mutex::new(42);

    {
        // Lock the mutex.
        let guard = mutex.lock().unwrap();

        // Ignore the guard.
        let _ = guard;

        // The lock is still held here.
        // The guard is dropped at the end of this block.
    }

    // Lock is released now.
    println!("{}", mutex.lock().unwrap());
}

Trust the drop order. The underscore respects scope boundaries.

Realistic usage: destructuring and side effects

You'll use the underscore constantly when destructuring tuples or structs where you only need a subset of the data. It keeps your code focused on what matters.

fn main() {
    // A record with id, name, and email.
    let record = (42, "Alice", "alice@example.com");

    // Destructure and ignore the name and email.
    let (id, _, _) = record;

    println!("User ID: {}", id);

    // In a struct, ignore specific fields.
    let config = Config {
        host: "localhost".to_string(),
        port: 8080,
        debug: false,
    };

    // We only need the host.
    let Config { host, .. } = config;

    println!("Connecting to {}", host);
}

struct Config {
    host: String,
    port: u16,
    debug: bool,
}

Convention aside: When a function returns a Result or a value you don't need, let _ = function_call(); is the standard signal to readers. It says, "I called this for side effects. I considered the return value and chose to discard it." This is clearer than just calling the function without binding, because the binding makes the discard intentional.

Use let _ = ... to signal that a return value is intentionally ignored.

Type inference with underscore

The underscore has a second life in type position. When you declare a variable, Rust usually needs the type. Sometimes the type is obvious to the compiler but tedious to write. You can use _ to tell the compiler to infer the type.

fn main() {
    // The compiler infers Vec<i32> from the literal.
    let numbers: _ = vec![1, 2, 3];

    println!("{:?}", numbers);
}

This is useful when the type is complex or when you're refactoring and want to avoid updating type annotations everywhere. It's also common in generic contexts where the type is determined by usage later.

fn main() {
    // The type of x is inferred from the push call.
    let mut x: _ = Vec::new();
    x.push(42);
}

Convention aside: The community uses _ for type inference sparingly. It's acceptable in local variables where the type is clear from context. Avoid it in function signatures or public APIs. Explicit types in signatures are part of the contract.

Pitfalls and compiler errors

The underscore is simple, but it has traps.

Unreachable patterns: The underscore matches everything. If you put _ before specific patterns in a match, those patterns become unreachable. The compiler warns you about this.

fn main() {
    let x = 1;
    match x {
        // `_` matches everything, including 1.
        _ => println!("anything"),
        // Warning: unreachable pattern.
        1 => println!("one"),
    }
}

Always put _ last in your match arms. If you need a default case, it goes at the bottom.

Underscore vs underscore-name: There's a subtle difference between _ and a name starting with underscore like _x. The underscore _ is a pattern that binds nothing. _x is a variable name that binds the value but suppresses the unused warning.

fn main() {
    let value = String::from("data");

    // `_` moves value and binds nothing.
    let _ = value;

    // `_x` moves value and binds it to _x.
    // The warning is suppressed, but the binding exists.
    let _x = value;
}

Use _x only when you need a binding to exist. This is rare. You might need it if you're implementing a trait and the compiler requires a binding for some reason, or if you want to extend the lifetime of a value without using it. In 99% of cases, _ is what you want.

Imports: Unused imports trigger warnings. You can suppress this with use ... as _;. This imports the item but tells the compiler you don't intend to use it. This is useful for re-exports or when you need a type in scope for a macro but don't reference it directly.

// Suppresses unused import warning.
use std::collections::HashMap as _;

Put _ last in match arms, or your specific cases will never run.

When to use underscore vs alternatives

Use _ to discard a value in a binding, pattern, or function argument. Use _ to match any remaining cases in a match expression. Use _ in type position to delegate type inference to the compiler. Use _name when you need to create a binding to satisfy a move or borrow constraint but won't read the value. Use .. to ignore multiple remaining fields in a struct or tuple struct.

Where to go next