Question
How to Check Debug vs Release Builds in Rust with cfg and cfg!
Question
In Rust, how can you detect whether the current build is a debug build or a release build using #[cfg(...)] or the cfg!(...) macro?
In C, this is commonly done with preprocessor checks such as:
#if defined(NDEBUG)
// release build
#endif
#if defined(DEBUG)
// debug build
#endif
With Cargo, the rough equivalents are:
cargo build # debug build
cargo build --release # release build
What is the Rust equivalent of this kind of conditional compilation?
I found that debug_assertions can be checked, but I am unsure whether that is the correct or most general way to distinguish debug and release behavior in Rust.
Short Answer
By the end of this page, you will understand how Rust handles conditional compilation for debug and release builds, when to use #[cfg(debug_assertions)] versus cfg!(debug_assertions), and how these differ from C-style preprocessor checks. You will also see practical examples, common mistakes, and typical usage patterns from real Rust codebases.
Concept
Rust does not have a C-style text preprocessor for #if defined(...) checks. Instead, it uses conditional compilation through the #[cfg(...)] attribute and the cfg!(...) macro.
The key idea is this:
#[cfg(...)]includes or excludes code at compile time.cfg!(...)evaluates totrueorfalsein an expression, but both branches must still be valid Rust code.
For distinguishing normal debug builds from optimized release builds, the most common built-in configuration is:
#[cfg(debug_assertions)]
This configuration is enabled when debug assertions are on, which is typically true for cargo build and typically false for cargo build --release.
So in practice:
cargo build->debug_assertionsis usually enabledcargo build --release->debug_assertionsis usually disabled
Mental Model
Think of #[cfg(...)] as a build-time gate.
If the condition matches, the compiler lets that code through the gate. If it does not match, the compiler acts as if that code was never there.
Think of cfg!(...) as a build-time fact turned into a boolean value.
It answers questions like:
- "Were debug assertions enabled when this code was compiled?"
- "Are we compiling for Windows?"
A simple analogy:
#[cfg(...)]is like deciding which rooms get built into a house.cfg!(...)is like asking, after the house is built, whether a certain room exists.
This difference matters because code behind #[cfg(...)] can be completely omitted, while code inside an if cfg!(...) { ... } else { ... } must still parse and type-check.
Syntax and Examples
Rust commonly uses debug_assertions for debug-vs-release conditional behavior.
Using #[cfg(...)]
Use this when you want code to exist only in certain builds.
#[cfg(debug_assertions)]
fn build_mode() {
println!("Debug build");
}
#[cfg(not(debug_assertions))]
fn build_mode() {
println!("Release build");
}
fn main() {
build_mode();
}
Why this works
- In a debug build, only the first
build_modefunction is compiled. - In a release build, only the second one is compiled.
Using cfg!(...)
Use this when you need a boolean inside an expression.
fn main() {
if cfg!(debug_assertions) {
println!("Debug build");
} else {
();
}
}
Step by Step Execution
Consider this example:
fn main() {
if cfg!(debug_assertions) {
println!("Debug build");
} else {
println!("Release build");
}
}
Here is what happens step by step:
- The compiler sees
cfg!(debug_assertions). - It checks whether the
debug_assertionsconfiguration is enabled for this build. - It replaces that macro result with a constant boolean value.
- In a typical debug build, this becomes
true. - In a typical release build, this becomes
false.
- In a typical debug build, this becomes
- The code behaves like this in a debug build:
fn main() {
if true {
println!("Debug build");
} else {
println!("Release build");
}
}
- Or like this in a release build:
() {
{
();
} {
();
}
}
Real World Use Cases
Here are common reasons to check debug vs release behavior in Rust:
Extra logging during development
if cfg!(debug_assertions) {
eprintln!("Loading configuration from test path");
}
Useful when debugging startup behavior without cluttering release output.
Expensive validation checks
#[cfg(debug_assertions)]
fn validate_state(values: &[i32]) {
assert!(values.windows(2).all(|w| w[0] <= w[1]));
}
This lets you catch logic bugs during development without paying the cost in production.
Debug-only helper functions
#[cfg(debug_assertions)]
fn dump_cache_state() {
println!("Dumping cache state for inspection...");
}
These helpers can be fully compiled out in release builds.
Testing internal assumptions
Libraries and applications often use debug_assert! or to verify assumptions that should never fail if the program is correct.
Real Codebase Usage
In real Rust projects, developers usually do not check for release mode directly. Instead, they express intent more clearly.
Common patterns
1. debug_assert! for development-time checks
fn divide(a: i32, b: i32) -> i32 {
debug_assert!(b != 0, "b should not be zero here");
a / b
}
This is preferred when the goal is to catch programming mistakes during development.
2. #[cfg(debug_assertions)] for debug-only code
#[cfg(debug_assertions)]
println!("Detailed parser state: {:?}", parser);
This is good for diagnostic output or expensive checks.
3. Guarding helper functions or modules
#[cfg(debug_assertions)]
mod debug_tools {
pub fn report() {
println!("Debug tools enabled");
}
}
This keeps development-only utilities out of production binaries.
Common Mistakes
Mistake 1: Thinking Rust has a C-style preprocessor
Broken mental model:
// This is not how Rust works
#if DEBUG
println!("debug");
#endif
Rust does not do source-text preprocessing like C. Use #[cfg(...)] or cfg!(...) instead.
Mistake 2: Using cfg!(...) when code should be excluded entirely
if cfg!(debug_assertions) {
use_debug_only_api();
} else {
normal_api();
}
Problem:
- Both branches must still compile.
- If
use_debug_only_api()is unavailable in release builds, this may fail.
Use #[cfg(...)] when code should not exist at all:
#[cfg(debug_assertions)]
use_debug_only_api();
#[cfg(not(debug_assertions))]
normal_api();
Mistake 3: Assuming is always identical to "not release"
Comparisons
| Tool | What it does | Best use | Important note |
|---|---|---|---|
#[cfg(debug_assertions)] | Includes code only when the condition matches | Compile out debug-only code | Non-matching code is not compiled |
cfg!(debug_assertions) | Returns true or false as a boolean | Conditional expressions inside functions | Both branches must still be valid |
debug_assert! | Assertion active when debug assertions are enabled | Internal correctness checks | Usually removed or disabled in release-style builds |
| Cargo feature flags | Custom compile-time switches | Optional functionality | Not the same as debug/release mode |
Cheat Sheet
Quick reference
Debug-only compilation
#[cfg(debug_assertions)]
fn debug_only() {
println!("Only compiled in debug-style builds");
}
Release-only compilation
#[cfg(not(debug_assertions))]
fn release_only() {
println!("Only compiled when debug assertions are off");
}
Boolean check inside expressions
if cfg!(debug_assertions) {
println!("Debug build");
}
Debug-only assertion
debug_assert!(value >= 0);
Rules to remember
#[cfg(...)]removes code from compilation when the condition does not match.cfg!(...)returns a boolean but does not remove surrounding code from type-checking.debug_assertionsis the normal Rust way to distinguish typical debug and release behavior.
FAQ
How do I check for a debug build in Rust?
Use #[cfg(debug_assertions)] or cfg!(debug_assertions). In normal Cargo workflows, this corresponds to typical debug builds.
How do I check for a release build in Rust?
Use #[cfg(not(debug_assertions))] or !cfg!(debug_assertions).
What is the difference between #[cfg] and cfg! in Rust?
#[cfg] conditionally includes or excludes code from compilation. cfg! returns a boolean value inside expressions.
Is debug_assertions the same as cargo build --release?
Not exactly. It indicates whether debug assertions are enabled. In default Cargo profiles, it usually lines up with debug vs release builds.
Should I use build checks for application logic?
Usually no. Restrict them to diagnostics, assertions, logging, or development-only validation.
Can I create my own build flags in Rust?
Yes. Common options include Cargo features, build scripts, and profile configuration.
Why does code inside if cfg!(...) still need to compile?
Mini Project
Description
Build a small Rust program that reports the current build mode and runs extra validation only when debug assertions are enabled. This demonstrates both forms of conditional compilation: #[cfg(...)] for code inclusion and cfg!(...) for boolean checks inside a function.
Goal
Create a program that prints whether it is running in debug or release style, and performs extra checks only in debug-enabled builds.
Requirements
- Print a message showing whether
debug_assertionsis enabled. - Add a debug-only function using
#[cfg(debug_assertions)]. - Add a release-only function using
#[cfg(not(debug_assertions))]. - Use
cfg!(debug_assertions)insidemain. - Run the program with both
cargo runandcargo run --releaseto compare behavior.
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Associated Types vs Generic Type Parameters in Rust: When to Use Each
Learn when to use associated types vs generic parameters in Rust traits, with clear rules, examples, and practical API design advice.
Can a Struct Extend Another Struct in Rust? Composition vs Inheritance
Learn how Rust handles struct reuse without inheritance, using composition, traits, and wrapper structs with practical examples.