The blueprint isn't the building
You're building a simple game loop. You define an enum to track state: Menu, Playing, and GameOver. You write state = GameState::Playing; and it works. Then you decide GameOver should store the final score. You change the enum to GameOver(i32). You update your code to state = GameState::GameOver;. The compiler rejects you with E0423: expected value, found struct variant.
You stare at the screen. GameOver is right there in the enum. The name hasn't changed. Why does Rust suddenly think it's not a value? You check the spelling. You check the imports. Nothing is wrong with the definition. The error feels like Rust is nitpicking grammar. It isn't. Rust is enforcing a distinction between the definition of a variant and the construction of a value. You pointed at the mold, but the code expects the part.
Constructors are functions
Rust treats enum variants that hold data as constructors. A constructor is a function that builds a value. When you define enum GameState { GameOver(i32) }, you are telling Rust that GameState::GameOver is a function that takes an i32 and returns a GameState.
If you write GameState::GameOver, you are referring to the constructor function itself. You are not calling it. In most contexts, Rust expects a value of type GameState, not a function that returns a GameState. The types do not match. The compiler sees you handed it a function where it needed a value, and it throws E0423.
This distinction exists because Rust is strict about types. A function has a different type than the value it produces. GameState::GameOver has the type fn(i32) -> GameState. GameState::GameOver(42) has the type GameState. E0423 fires when you confuse the two.
Minimal example: The missing parentheses
The most common trigger for E0423 is forgetting parentheses on a tuple variant. Tuple variants hold data in a position-based tuple. They require parentheses to construct.
enum Token {
Number(f64),
Keyword(String),
Eof,
}
fn main() {
// E0423: expected value, found struct variant
// Token::Number is a constructor, not a value.
// It requires an f64 argument.
let t = Token::Number;
// Fix: Call the constructor with data.
let t = Token::Number(3.14);
}
The error message "expected value, found struct variant" tells you exactly what happened. The compiler expected a value of type Token. It found Token::Number, which is a struct variant constructor. Since Number holds data, Rust requires you to call it. Adding the parentheses and the argument fixes the error.
Walkthrough: Types and constructors
Understanding E0423 requires seeing how Rust models variants internally. Every variant is a distinct type constructor. When you define an enum, Rust generates a constructor for each variant.
For a unit variant like Eof, the constructor takes no arguments. Token::Eof is a zero-argument function that returns Token. You can write Token::Eof or Token::Eof(). Both work. The bare name is idiomatic because it reads like a value.
For a tuple variant like Number(f64), the constructor takes one argument. Token::Number is a function that takes f64 and returns Token. If you write Token::Number, you are referring to the function. If you write Token::Number(3.14), you are calling the function and getting a value.
This function-like behavior is powerful. You can assign constructors to variables. This is a convention in functional-style Rust code.
enum Status {
Ok,
Err(String),
}
fn main() {
// Assign the constructor to a variable.
// make_err has type fn(String) -> Status.
let make_err = Status::Err;
// Call the variable like a function.
let s = make_err("timeout".to_string());
}
This works because Status::Err is a value in its own right: it is a function pointer. E0423 only appears when you try to use that function pointer in a place that expects a Status value. The compiler cannot coerce the function into the result type automatically. You must call it.
Realistic example: A message bus
Consider a message bus for a UI framework. Messages carry different payloads. You define an enum with tuple and struct variants.
#[derive(Debug)]
enum Message {
Click { x: i32, y: i32 },
Key(String),
Resize(i32, i32),
Quit,
}
fn dispatch(msg: Message) {
println!("Dispatching: {:?}", msg);
}
fn main() {
// E0423: expected value, found struct variant
// Message::Key is a constructor expecting a String.
dispatch(Message::Key);
// E0423: expected value, found struct variant
// Message::Resize is a constructor expecting two i32s.
dispatch(Message::Resize);
}
The compiler rejects both calls. Message::Key and Message::Resize are constructors. You need to provide the data.
fn main() {
// Fix: Provide the String argument.
dispatch(Message::Key("Enter".to_string()));
// Fix: Provide the two i32 arguments.
dispatch(Message::Resize(800, 600));
// Unit variant works with bare name.
dispatch(Message::Quit);
}
Convention aside: Use struct variants like Click { x, y } when the data has named meaning. Use tuple variants like Resize(i32, i32) when the data is a homogeneous pair or order matters. Struct variants improve readability and allow partial updates in some contexts. Tuple variants are concise for simple data bundles.
Struct variants and the brace trap
Struct variants hold data in named fields. They require braces to construct. Forgetting braces triggers E0423, just like forgetting parentheses.
enum Config {
Debug { level: u8, verbose: bool },
Release,
}
fn main() {
// E0423: expected value, found struct variant
// Config::Debug is a constructor expecting named fields.
let c = Config::Debug;
// Fix: Use braces and field names.
let c = Config::Debug { level: 2, verbose: true };
}
The error message is identical. "Expected value, found struct variant." The fix is to use the struct literal syntax. You must provide all fields unless you use ..Default::default() or a similar update syntax, which requires an existing value.
Patterns vs expressions
A major source of confusion is the similarity between construction syntax and pattern syntax. In a match arm, you use variant names to deconstruct values. The syntax looks like construction, but it is a pattern.
enum Result<T, E> {
Ok(T),
Err(E),
}
fn main() {
let r: Result<i32, String> = Ok(42);
match r {
// Pattern: Ok(value) deconstructs the variant.
// No E0423 here. This is not construction.
Ok(value) => println!("Got {}", value),
Err(e) => println!("Error: {}", e),
}
}
E0423 is an expression error. It appears when you are building values, not matching them. You will not see E0423 inside a match arm pattern. If you see E0423, you are likely in an expression context: a let binding, a function argument, a return value, or a field initializer.
Check your context. If you are inside a match, an if let, or a while let, you are writing patterns. The syntax is different. If you are assigning to a variable or calling a function, you are writing expressions. You must construct the value.
Unit variants and the empty parens debate
Unit variants carry no data. They are like constants. You can construct them with the bare name or with empty parentheses.
enum Color {
Red,
Green,
Blue,
}
fn main() {
// Idiomatic: bare name.
let c1 = Color::Red;
// Valid but noisy: empty parens.
let c2 = Color::Green();
// Both compile. Both produce the same value.
}
Both forms compile. The community convention is to use the bare name for unit variants. Color::Red reads like a value. Color::Red() reads like a function call. Since no data is involved, the parentheses add noise without adding information. Stick to the bare name unless you have a specific reason to prefer consistency across all variants.
Some linters flag Color::Red() as unnecessary. cargo clippy may suggest removing the parentheses. Following convention keeps your code aligned with the broader Rust ecosystem.
Pitfalls and E0423
E0423 appears in several scenarios. The core issue is always the same: you used a variant constructor where a value was expected.
The compiler rejects this with E0423 when you omit parentheses on a tuple variant. It rejects this with E0423 when you omit braces on a struct variant. It rejects this with E0423 when you use a variant name in a function call position without arguments.
A subtle pitfall involves shadowing. If you define a function with the same name as a variant, the function shadows the variant. This can lead to confusing errors.
enum Msg {
Ping,
}
fn Ping() -> Msg {
Msg::Ping
}
fn main() {
// This calls the function Ping, not the variant.
// It works, but it's confusing.
let m = Ping();
}
Avoid naming functions the same as variants. It creates ambiguity. Use distinct names. The compiler will resolve the shadowing, but readers will struggle.
Another pitfall is using Enum::Variant in a type position. You cannot use a variant name as a type. Types are the enum itself, not the variant.
enum Shape {
Circle(f64),
Square(f64),
}
fn area(s: Shape::Circle) {
// E0423 or type error.
// Shape::Circle is not a type.
}
The type is Shape. Variants are values within the type. You cannot pass a variant type to a function. You pass the enum type and match on the variant inside.
Decision matrix
Use the bare variant name Enum::Variant when the variant is a unit variant and you need a value. This is the idiomatic form for constants.
Use Enum::Variant(value) when the variant is a tuple variant and you are constructing it with positional data. Provide all arguments in order.
Use Enum::Variant { field: value } when the variant is a struct variant and you are constructing it with named fields. Provide all fields unless using update syntax.
Use Enum::Variant() for unit variants only if you need to disambiguate or prefer explicit construction syntax, but prefer the bare name for readability.
Reach for match patterns when you are deconstructing values, not building them. The syntax mirrors construction but operates in pattern context.
Treat variant names as constructors. If they take arguments, call them. If you forget, the compiler will remind you with E0423. Check your parentheses and braces. The compiler isn't mad at your logic. It's mad at your grammar.