Question

Convert a Rust Enum to a User-Facing String

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.

Take Quiz

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:

  1. value stores the enum variant Foo::Quux.
  2. value.as_str() matches Foo::Quux and returns the borrowed string slice "Quux".
  3. label refers to that static string data; no new string allocation is needed.
  4. format!("I am {}", value) sees {} and uses Foo's Display implementation.
  5. Display::fmt matches Foo::Quux, chooses "Quux", and writes it into the formatter.
  6. format! returns an owned String containing "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 Display for 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".

Take Quiz

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

ApproachBest useProducesNotes
{:?} / DebugDiagnostics and developer logsFormatted outputDo not rely on it as user-facing wording.
{} / DisplayMessages, reports, normal outputFormatted outputIdiomatic choice for user-facing representation.
as_str() -> &'static strFixed labels, comparisons, length checksBorrowed string sliceNo allocation; you define the mapping.
to_string()When an owned String is requiredStringUses Display; allocates for the new string.
as u8 or another integer castNumeric protocol or storage codesIntegerNot 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.

Take Quiz

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 {:?} for Debug; use {} for Display.
  • to_string() requires an implementation of Display (or another ToString implementation).
  • Return &'static str for constant labels stored in the program binary.
  • A match over 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.

Take Quiz

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

:[

Take Quiz

Keep learning

Related questions

View more