Determining debug/release builds in Rust via cfg: a complete guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

How to distinguish debug from release in Rust? Let's break down cfg with real examples

\n\n

Have you ever written code in Rust and thought: "I wish I could add detailed logging in debug mode and remove all the extras in release"? Or maybe you wanted to include assertions only during development but disable them in the final build? If so — you've come to the right place. Today we'll explore how to use the magic of cfg to control code behavior depending on the build type. This isn't just dry theory — we'll go from a simple flag to a production-ready solution.

\n\n

This topic isn't pulled out of thin air: on Stack Overflow, the question "How to check release / debug builds using cfg in Rust?" has gathered thousands of views. That means many people need this. And we'll break it down thoroughly.

\n\n

Why is this important? Two worlds — two approaches

\n\n

In Rust, there are two main build profiles: debug (with the --debug flag or simply cargo build) and release (cargo build --release). They differ not only in optimizations but also in the behavior of some built-in checks.

\n\n

For example:

\n
    \n
  • debug: slow, but with array bounds checks, debug symbols, and assertions.
  • \n
  • release: fast, but without extra checks — all for performance.
  • \n
\n\n

What if you need the debug version to log every action, while the release version only logs critical errors? Or if you want a test endpoint to be accessible only locally? That's exactly what cfg is for.

\n\n

Main tools: cfg!() and #[cfg()]

\n\n

In Rust, there are two ways to check the build type:

\n
    \n
  • The cfg!() macro — executed at compile time, returns a bool. Used inside functions.
  • \n
  • The #[cfg()] attribute — completely includes or excludes a block of code from compilation.
  • \n
\n\n

Both use the same keys. The most popular one for our task is debug_assertions. It is active only in debug builds. There is also its inverse — not(debug_assertions) for release.

\n\n

Example 1: Simple check inside a function

\n\n

Let's start with the cfg!() macro. Suppose we have a function that calculates something complex. In debug, we want to validate the input data:

\n\n
fn calculate_something(x: i32) -> i32 {\n    // Check only in debug builds\n    if cfg!(debug_assertions) {\n        println!("[DEBUG] calculate_something called with x = {}", x);\n        assert!(x > 0, "x must be positive!");\n    }\n\n    // Main logic\n    x * 2\n}\n\nfn main() {\n    let result = calculate_something(10);\n    println!("Result: {}", result);\n}\n
\n\n

What will happen?

\n
    \n
  • With cargo build (debug) — you'll see the log, and if x ≤ 0, the program will panic with an assertion.
  • \n
  • With cargo build --release — the line with cfg!(debug_assertions) will evaluate to false, and the compiler will eliminate this block as dead code. No overhead!
  • \n
\n\n

Example 2: Complete code exclusion with #[cfg()]

\n\n

Sometimes you need not just to skip execution, but to completely remove a function or module from the binary. For this, we use the attribute:

\n\n
// This module will be compiled ONLY in debug builds\n#[cfg(debug_assertions)]\nmod debug_helpers {\n    pub fn print_state(state: &str) {\n        println!("[DEBUG] Current state: {}", state);\n    }\n\n    pub fn validate_data(data: &[u8]) -> bool {\n        // Expensive check, not needed in release\n        data.len() < 1000\n    }\n}\n\n// This module is only for release\n#[cfg(not(debug_assertions))]\nmod release_helpers {\n    pub fn print_state(_state: &str) {\n        // Do nothing in release — save CPU\n    }\n\n    pub fn validate_data(_data: &[u8]) -                

Blogs

Book Recommendations