1. Symptoms
When Rust compiler error E0438 occurs, the compiler produces a diagnostic message indicating that it expected a semicolon (;) or assignment operator (=) after encountering an expression. This typically manifests when macro-generated code produces an incomplete statement that lacks proper termination.
The error manifests with a message structure similar to the following:
error[E0438]: expected `;` or `=` after expression
--> src/main.rs:12:5
|
12 | some_macro!(arg1 arg2)
| ^^^^^^^^^^^^^^^^^^^^^ expected `;` or `=`
In proc-macro contexts, the error may appear with slightly different wording but maintains the core semantics of expression termination failure. The error points to the macro invocation site rather than the macro definition itself, since the problem manifests during macro expansion evaluation.
Developers frequently encounter this error when attempting to pass multiple arguments to macros without proper comma separation, when macro output generates trailing expressions without statement terminators, or when using procedural macros that emit incomplete syntax trees. The error location usually points to the line where the macro is invoked, but the root cause often lies in how the macro constructs its output or how the macro invocation is syntactically structured.
2. Root Cause
The fundamental cause of error E0438 is a mismatch between what the Rust parser expects at a particular syntax position and what the macro actually produces. The Rust grammar requires that expressions used as statements must be terminated with either a semicolon or be the last expression in a block that implicitly returns. When a macro invocation results in code that violates this expectation, the compiler emits E0438.
There are three primary scenarios that trigger this error. The first involves macro invocation syntax itself. When using macro_rules! macros with token matching, if you omit required commas between arguments or structure the invocation incorrectly, the parser cannot properly recognize the macro call. For example, attempting to pass space-separated arguments where the macro definition expects comma-separated arguments causes the parser to interpret the tokens differently than intended.
The second scenario occurs within macro definitions themselves. When a macro expands to an expression that serves as a statement, the expansion must include a terminating semicolon unless that expression is the final expression of a block. A macro that generates something like let x = compute_value() without a semicolon in a statement context will trigger this error because the parser expects the statement to be terminated.
The third scenario involves procedural macros that construct syntax trees incorrectly. A proc-macro that generates an Expr node when the context requires an ExprStmt, or that omits the semicolon token in the syntax tree, will cause this error. The procedural macro API requires explicit construction of the terminating semicolon when needed.
3. Step-by-Step Fix
Fix for Incorrect Macro Invocation Syntax
Before:
macro_rules! my_macro {
($a:expr, $b:expr) => {
println!("A: {}, B: {}", $a, $b);
};
}
fn main() {
my_macro!(42 84); // Missing comma between arguments
}
After:
macro_rules! my_macro {
($a:expr, $b:expr) => {
println!("A: {}, B: {}", $a, $b);
};
}
fn main() {
my_macro!(42, 84); // Correct comma separation
}
Fix for Macro Definitions Missing Statement Terminators
Before:
macro_rules! compute_and_print {
($val:expr) => {
let result = $val * 2
}; // Missing semicolon after expression
}
fn main() {
compute_and_print!(21);
}
After:
macro_rules! compute_and_print {
($val:expr) => {
let result = $val * 2
}; // Now correctly expects semicolon at invocation site
}
fn main() {
compute_and_print!(21); // Semicolon automatically provided by context
}
Fix for Properly Terminated Macro Expression Output
Before:
macro_rules! expression_macro {
($x:expr) => {
$x + 1
};
}
fn main() {
let y = 5;
expression_macro!(y); // Expression result not terminated
}
After:
macro_rules! expression_macro {
($x:expr) => {
$x + 1
};
}
fn main() {
let y = 5;
let z = expression_macro!(y); // Wrap in assignment context
}
Fix for Procedural Macro Incorrect Syntax Tree
Before:
// In proc-macro crate
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro]
pub fn bad_macro(input: TokenStream) -> TokenStream {
let input_str = input.to_string();
// Incorrectly generates expression without semicolon for statement context
quote! {
some_function(#input_str)
}.into()
}
After:
// In proc-macro crate
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro]
pub fn good_macro(input: TokenStream) -> TokenStream {
let input_str = input.to_string();
// Generates expression with explicit semicolon for statement context
quote! {
some_function(#input_str);
}.into()
}
4. Verification
After applying the appropriate fix, verify resolution by compiling the project:
cargo build
A successful compilation with no errors confirms the fix. If the project includes tests, running them provides additional verification:
cargo test
Examine the specific test cases that previously triggered E0438 to ensure they now compile and execute correctly. For proc-macro changes, verify that the generated syntax tree is valid by examining the expanded code:
cargo expand
The expanded output should show properly terminated expressions where previously the syntax was incomplete. Additionally, run the linter to catch any remaining style issues:
cargo clippy
If using an IDE with Rust language support, the error indicators should disappear immediately upon editing, confirming the syntactical correction.
5. Common Pitfalls
One common mistake is assuming the error originates from the macro definition when it actually stems from how the macro is invoked. Developers frequently modify the macro body attempting to add semicolons, only to discover the problem was incorrect invocation syntax. Always examine the error span carefully to determine whether the issue lies in the definition or the call site.
Another pitfall involves treating macro_rules! patterns as function calls. Unlike function calls, macro invocations have strict rules about token separation. Spaces do not serve as argument separators in macro contexts; you must use commas (or whatever separator the macro pattern specifically expects). Attempting to call a macro like my_macro!(a b c) when the pattern expects ($a:expr, $b:expr, $c:expr) will not work as intended.
When working with procedural macros, a subtle mistake involves forgetting that the output must include terminating tokens for the syntactic context. A proc-macro that generates code intended for use as a statement must explicitly emit the semicolon token. The quote! macro will not automatically add terminators; you must include them explicitly in the quote template.
Finally, avoid the temptation to use expr; as a workaround within macro definitions when the macro is used in expression contexts where semicolons are prohibited. Blocks in Rust have specific semantics regarding the final expression, and adding a semicolon changes the block type from expression to statement. Understand whether your macro is meant to produce statements or expressions, and design accordingly.
6. Related Errors
E0423: Expected expression, found statement — This error occurs when the Rust compiler encounters a statement where it expects an expression, often in contexts like array initialization where the compiler requires a value-producing expression rather than a terminating statement. E0423 and E0438 are complementary errors that both relate to expression/statement confusion, with E0423 indicating the opposite problem from E0438.
E0437: Expected type, found expression — This error surfaces when an expression appears in a position requiring a type annotation, such as after a colon in a type position. While not directly related to macros, this error commonly occurs in generated code where proc-macros emit the wrong node type, similar in mechanism to E0438.
E0451: Found ;, expected expression — This error indicates the inverse situation of E0438, where a semicolon is found where an expression is required. When macro-generated code incorrectly places semicolons in expression contexts (such as in the tail position of a block), E0451 appears, highlighting the dual nature of the expression/statement distinction that E0438 addresses.