Question
Given a Rust enum with many variants, how can I obtain the name or a user-facing label for a variant as a String or &str?
enum Foo {
Bar = 0x00,
Baz = 0x01,
Qux = 0x02,
// ...
Quux = 0xFF,
}
Debug formatting can be used as follows:
let text = format!("I am {:?}", Foo::Quux);
This produces I am Quux, but Debug is not appropriate for user-facing output. I also need the enum text as a value so that I can measure its length and use it in custom formatting calculations.
Short Answer
You will learn how Rust separates developer-oriented Debug output from user-facing Display output. You will also see how to implement Display for an enum, return a stable &str label when needed, and convert formatted output into an owned String.
Concept
Rust enums do not automatically have a built-in conversion from every variant to its source-code name. This is intentional: a variant name such as Quux is a programming identifier, while user-facing text may need spaces, different capitalization, localization, or wording that can change independently.
Rust provides formatting traits for different purposes:
Debug({:?}) is primarily for developers, diagnostics, and debugging.Display({}) represents text intended for users or normal program output.
For user-facing enum text, implement std::fmt::Display. The implementation chooses exactly what each variant means as text. Once Display exists, format!("{}", value) produces a String, and value.to_string() does as well.
If you specifically need a borrowed string for length calculations or to avoid allocation, add a method that returns &'static str. This is often the clearest choice for a fixed set of labels.
Mental Model
Think of an enum variant as a product code on a shelf: Foo::Quux identifies which item the program has. The product code is useful internally, but customers need a label such as Quux, Special option, or a translated name.
Debug is like a warehouse label meant for staff. Display is the label shown to customers. A method returning &str is like reading the printed label directly, while to_string() makes a new copy of that label for you to store or modify.
Syntax and Examples
Implement Display by matching every enum variant and writing its chosen label to the formatter.
use std::fmt;
#[derive(Debug, Clone, Copy)]
enum Foo {
Bar,
Baz,
Qux,
Quux,
}
impl fmt::Display for Foo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Foo::Bar => "Bar",
Foo::Baz => "Baz",
Foo::Qux => "Qux",
Foo::Quux => "Quux",
};
f.write_str(label)
}
}
fn main() {
let value = Foo::Quux;
println!("I am {}", value);
let text: String = value.to_string();
println!("{} has {} characters", text, text.len());
}
Display::fmt receives a formatter, not a String. f.write_str(label) sends the selected text to whichever destination is formatting the value: a String, standard output, a file, or another writer.
For a fixed label that you want to inspect without allocating, expose it directly:
impl Foo {
fn as_str(self) -> &'static str {
match self {
Foo::Bar => "Bar",
Foo::Baz => "Baz",
Foo::Qux => "Qux",
Foo::Quux => "Quux",
}
}
}
let label = Foo::Quux.as_str();
assert_eq!(label, "Quux");
assert_eq!(label.len(), 4);
You can use both patterns: make as_str the single source of labels, then have Display call it.
Step by Step Execution
Consider this code:
let value = Foo::Quux;
let label = value.as_str();
let message = format!("I am {}", value);
Execution proceeds as follows:
valuestores the enum variantFoo::Quux.value.as_str()matchesFoo::Quuxand returns the borrowed string slice"Quux".labelrefers to that static string data; no new string allocation is needed.format!("I am {}", value)sees{}and usesFoo'sDisplayimplementation.Display::fmtmatchesFoo::Quux, chooses"Quux", and writes it into the formatter.format!returns an ownedStringcontaining"I am Quux".
A full reusable implementation is:
use std::fmt;
#[derive(Clone, Copy)]
enum Foo {
Bar,
Baz,
Qux,
Quux,
}
impl Foo {
fn as_str(self) -> &'static str {
match self {
Self::Bar => "Bar",
Self::Baz => "Baz",
Self::Qux => "Qux",
Self::Quux => "Quux",
}
}
}
impl fmt::Display for Foo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str((*self).as_str())
}
}
Foo derives Copy, so *self can be passed by value to as_str without moving the original value.
Real World Use Cases
Common uses for enum-to-text conversion include:
- CLI applications: show a selected mode such as
"Safe mode"or"Fast mode". - Status messages: display an order state, job result, or connection state.
- Logs and reports: format domain values in readable messages.
- Serialization helpers: map internal variants to protocol or configuration labels when a dedicated serialization format is not being used.
- Tables and aligned output: use
as_str().len()or display-width-aware logic to calculate column sizes. - Error messages: implement
Displayfor an error enum so callers receive useful messages.
For text shown to people, labels do not need to match Rust variant identifiers. For example, Foo::Quux could display as "Experimental mode".
Real Codebase Usage
In production code, a common pattern is to make one method responsible for the canonical label and reuse it from Display.
use std::fmt;
#[derive(Clone, Copy)]
enum Status {
Pending,
Running,
Complete,
Failed,
}
impl Status {
fn as_str(self) -> &'static str {
match self {
Self::Pending => "Pending",
Self::Running => "Running",
Self::Complete => "Complete",
Self::Failed => "Failed",
}
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str((*self).as_str())
}
}
This prevents duplicated mappings. Code that needs formatting uses {status}; code that needs a borrowed label uses status.as_str().
For a writer implementing std::io::Write, build the output and use write_all when you need all bytes written:
use std::io::{self, Write};
fn write_status(mut writer: impl Write, status: Status) -> io::Result<()> {
writeln!(writer, "Current status: {status}")
}
writeln! writes through the writer and uses Display automatically. If a project needs translated messages, avoid treating variant names as final user text; route labels through a localization layer instead.
Common Mistakes
Using Debug as a user-facing contract
println!("Status: {:?}", Foo::Quux);
This works, but Debug output is intended for developers and may not be appropriate wording for users. Implement Display and use {} instead.
Expecting a cast to produce text
// This produces the numeric discriminant, not "Quux".
let code = Foo::Quux as u8;
A numeric discriminant and a display label are different things. Use a match, Display, or as_str for text.
Repeating the match in several places
let label = match value {
Foo::Bar => "Bar",
Foo::Baz => "Baz",
Foo::Qux => "Qux",
Foo::Quux => "Quux",
};
A local match is fine once, but repeating it can cause inconsistent labels later. Put the mapping in as_str and reuse it.
Allocating when a borrowed string is enough
let length = value.to_string().len();
This creates a String only to count bytes. When labels are fixed, prefer:
let length = value.as_str().len();
Assuming len() counts visible characters
str::len() returns the number of UTF-8 bytes, not necessarily the number of user-visible characters or terminal columns. It is correct for ASCII labels such as "Quux". For non-ASCII UI text, display-width calculations may need a Unicode-aware approach.
Comparisons
| Approach | Best use | Produces | Notes |
|---|---|---|---|
{:?} / Debug | Diagnostics and developer logs | Formatted output | Do not rely on it as user-facing wording. |
{} / Display | Messages, reports, normal output | Formatted output | Idiomatic choice for user-facing representation. |
as_str() -> &'static str | Fixed labels, comparisons, length checks | Borrowed string slice | No allocation; you define the mapping. |
to_string() | When an owned String is required | String | Uses Display; allocates for the new string. |
as u8 or another integer cast | Numeric protocol or storage codes | Integer | Not a textual name. |
Display and as_str are complementary rather than competing. Use Display for generic formatting and as_str when code specifically needs the label as a string slice.
Cheat Sheet
use std::fmt;
#[derive(Clone, Copy)]
enum Level { Low, High }
impl Level {
fn as_str(self) -> &'static str {
match self {
Self::Low => "Low",
Self::High => "High",
}
}
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str((*self).as_str())
}
}
let level = Level::High;
println!("{level}"); // Display: High
let owned = level.to_string(); // String: "High"
let borrowed = level.as_str(); // &str: "High"
let byte_count = borrowed.len(); // 4 for ASCII text
- Use
{:?}forDebug; use{}forDisplay. to_string()requires an implementation ofDisplay(or anotherToStringimplementation).- Return
&'static strfor constant labels stored in the program binary. - A
matchover an enum is checked by the compiler: adding a variant requires handling it. - Integer discriminants are not automatically user-facing names.
FAQ
Can Rust automatically turn an enum variant into its name?
Not as a general built-in conversion. Implement Display or an as_str method so your program explicitly defines the text.
Should I use Debug or Display for user output?
Use Display. Debug is designed for developer-oriented representations.
How do I get a String from an enum in Rust?
Implement Display, then call value.to_string() or format!("{}", value).
How do I avoid allocating a String just to get the label?
Implement fn as_str(self) -> &'static str and use the returned string slice directly.
Does as_str().len() return the character count?
It returns UTF-8 byte length. For ASCII labels, byte length equals character count. Non-ASCII text may need Unicode-aware counting or terminal-width calculation.
Can displayed text differ from the enum variant name?
Yes. That is often desirable. For example, Status::InProgress can display as "In progress".
Why does the Display implementation use fmt::Result?
Formatting can write to destinations that may fail. The formatting trait requires returning that result so errors can propagate correctly.
Mini Project
Description
Create a small order-status formatter for a command-line report. The program must provide readable labels for users while also allowing code to inspect label lengths for aligned output.
Goal
Implement an enum that supports both {} formatting and allocation-free access to its user-facing label.
Requirements
:[
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.