When the build should stop
You are writing a crate that wraps a Linux-specific system call. You publish it. A user on Windows tries to compile their project. They get a linker error about a missing symbol. They open an issue. You reply that the crate only supports Linux. The user closes the issue, frustrated.
You could have prevented this. You could have placed a check at the top of the file that halts compilation with a clear message. The user sees the message, checks the documentation, and moves on. No issue filed. No wasted time.
That is what compile_error! does. It is a macro that stops the build immediately with a message you control. It is not a runtime check. It is a compile-time assertion. If the compiler reaches this macro, the build fails. No binary is produced. The user sees your message instead of a cryptic linker error or a confusing trait bound failure.
The concept: fail fast at the gate
Rust encourages failing fast. The borrow checker stops you from writing unsafe code before you run it. compile_error! extends that philosophy to configuration and environment constraints. It lets you define invariants about the build environment and enforce them with a message that makes sense to a human.
Think of compile_error! as a red flag on a blueprint. The builder sees the flag and stops before pouring concrete. A runtime panic! is the building collapsing after it is constructed. You want the red flag. You want the user to know exactly why the build failed and how to fix it, before they waste time debugging.
The macro takes a single string literal. It expands to nothing. It does not generate code. It injects an error into the compiler's diagnostic stream. The error includes your message and the location in the source code. The build halts.
Minimal example
Here is the simplest usage. The macro stops the build with your message.
fn main() {
// This macro halts compilation immediately.
// No binary is produced.
compile_error!("Build aborted: this code is broken by design.");
}
If you run cargo build, the output looks like this:
error: Build aborted: this code is broken by design.
--> src/main.rs:3:5
|
3 | compile_error!("Build aborted: this code is broken by design.");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The compiler points to the macro call. The message is exactly what you wrote. The build fails.
How the compiler processes it
When the compiler parses your code, it expands macros. Most macros generate tokens that become part of the program. compile_error! is different. It generates no tokens. Instead, it records an error.
The compiler continues parsing the rest of the file to find other errors, but it will not produce a binary. The error propagates to the build system. Cargo reports the error and exits with a non-zero status.
This behavior makes compile_error! safe to use in conditional compilation. You can wrap it in cfg attributes. If the condition is false, the macro is ignored. If the condition is true, the macro runs and the build fails.
Realistic examples
Enforcing feature flags
Crates often use feature flags to enable optional functionality. If a user tries to use a function that requires a feature, you can stop them with a clear message.
/// This module requires the 'json' feature.
/// If the feature is disabled, compilation fails.
#[cfg(not(feature = "json"))]
compile_error!("Enable the 'json' feature to use this module.");
#[cfg(feature = "json")]
pub fn parse_json(input: &str) -> Result<serde_json::Value, serde_json::Error> {
// Implementation only available when the feature is enabled.
serde_json::from_str(input)
}
The cfg attribute checks if the json feature is disabled. If it is, the macro runs. The user sees the message and knows to add features = ["json"] to their Cargo.toml. If the feature is enabled, the macro is ignored and the function is available.
Checking platform support
Some code only works on specific operating systems. You can enforce this constraint at compile time.
/// This driver only supports Linux.
/// Compilation fails on other platforms.
#[cfg(not(target_os = "linux"))]
compile_error!("This driver requires Linux. Check the docs for cross-platform alternatives.");
#[cfg(target_os = "linux")]
pub fn init_driver() {
// Linux-specific initialization code.
// This block is only compiled on Linux.
}
The cfg attribute checks the target OS. If the user tries to build on Windows or macOS, the macro runs. The message points them to alternatives. This is much better than a linker error or a runtime panic.
Providing better macro errors
This is where compile_error! shines. When you write a macro, the compiler might give cryptic errors if the user passes invalid arguments. You can use compile_error! to provide a human-readable message.
/// A macro that requires a specific feature to be enabled.
/// If the feature is missing, it emits a clear error.
macro_rules! require_feature {
($feature:ident) => {
// Check if the feature is disabled.
// If so, emit a compile error with the feature name.
#[cfg(not(feature = $feature))]
compile_error!(concat!("Feature '", stringify!($feature), "' is required."));
};
}
// Usage in your crate:
// require_feature!(serde);
The macro uses cfg to check the feature. If the feature is disabled, it calls compile_error!. The concat! and stringify! macros build the message at compile time. stringify! converts the identifier to a string. concat! joins the parts into a single literal.
The user sees: error: Feature 'serde' is required. This is far more helpful than a trait bound error or a missing function error.
Pitfalls and limitations
No string interpolation
compile_error! takes a string literal. You cannot pass variables or use format strings. This is a common mistake.
let version = "1.0.0";
// This does not compile.
// compile_error! expects a literal, not a variable.
// compile_error!("Version is {}", version);
If you need to include dynamic information, you must build the string at compile time using concat! or stringify!. This works inside macros where the values are known at expansion time.
macro_rules! check_version {
($expected:literal) => {
// Build the message at macro expansion time.
// The literal is known when the macro runs.
compile_error!(concat!("Expected version ", $expected, " but found incompatible config."));
};
}
Overuse leads to noise
Do not use compile_error! for logic errors that should be handled at runtime. It is for configuration and environment constraints. If a user passes invalid input to a function, use panic! or return an Err. compile_error! stops the build. It is too aggressive for runtime logic.
Interaction with cfg
Be careful with cfg attributes. The macro must be reachable for the error to trigger. If you nest cfg attributes incorrectly, the macro might be ignored even when the condition is true.
// This macro is ignored if 'debug' is enabled.
// The error never triggers.
#[cfg(debug_assertions)]
#[cfg(not(feature = "json"))]
compile_error!("JSON feature required in debug mode.");
The first cfg checks debug_assertions. If debug assertions are disabled, the whole block is ignored. The second cfg never runs. Make sure your conditions are correct.
Error codes
The compiler does not assign a specific error code to compile_error!. The error message is the code. Sometimes the compiler adds context, but the message you provide is what the user sees. Do not rely on error codes for compile_error!. Focus on making the message clear and actionable.
Decision matrix
Use compile_error! when the code cannot possibly run due to missing features, wrong platform, or invalid configuration. Use panic! when a runtime invariant is violated and recovery is impossible. Use assert! for debugging assumptions that should hold in development but can be optimized away in release. Use unimplemented! when you are writing a function stub and haven't implemented the logic yet. Use cfg attributes to hide entire modules or functions when the code is irrelevant to the target, rather than showing an error.
Conventions and community norms
Error messages should be actionable. Tell the user what to do. "Enable the 'json' feature" is better than "JSON feature missing." Capitalize the first letter. End with a period. Keep the message concise. Long messages wrap poorly in terminals.
The community treats compile_error! as a kindness to users. A clear compile error saves hours of debugging. When you write a macro, always consider adding compile_error! for common misuse cases. It makes your crate easier to use.
Convention aside: compile_error! messages are often copy-pasted into search engines. Include the name of the feature or platform in the message. Users will search for the error. If the message contains the feature name, they find the solution faster.