318: Display vs Debug for Errors
Tutorial Video
Text description (accessibility)
This video demonstrates the "318: Display vs Debug for Errors" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Error messages have two audiences: end users who need human-readable descriptions ("Cannot connect to server"), and developers who need complete diagnostic information including internal state (field names, codes, stack frames). Key difference from OCaml: 1. **Two required traits**: Rust's `std::error::Error` requires both `Display` and `Debug`; OCaml has no such requirement — any type can be an error.
Tutorial
The Problem
Error messages have two audiences: end users who need human-readable descriptions ("Cannot connect to server"), and developers who need complete diagnostic information including internal state (field names, codes, stack frames). Rust encodes this distinction in two traits: Display for user-facing messages, Debug for developer diagnostics. Both are required by std::error::Error, and using them correctly separates user experience from debugging information.
🎯 Learning Outcomes
Display as user-facing output (error messages shown to end users)Debug as developer-facing output (detailed diagnostic information){:?} uses Debug, {} uses Display in format stringsCode Example
#![allow(clippy::all)]
//! # Error Display vs Debug
//!
//! Display is for users, Debug is for developers.
use std::fmt;
#[derive(Debug)]
pub enum DbError {
ConnectionFailed(String),
QueryTimeout(f64),
NotFound(String),
}
impl fmt::Display for DbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ConnectionFailed(h) => write!(f, "Cannot connect to {h}"),
Self::QueryTimeout(s) => write!(f, "Query timed out after {s:.1}s"),
Self::NotFound(k) => write!(f, "Record not found: {k}"),
}
}
}
impl std::error::Error for DbError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_human_readable() {
let e = DbError::ConnectionFailed("localhost".into());
assert_eq!(e.to_string(), "Cannot connect to localhost");
}
#[test]
fn test_debug_has_variant() {
let e = DbError::NotFound("x".into());
let debug = format!("{:?}", e);
assert!(debug.contains("NotFound"));
}
#[test]
fn test_implements_error() {
let e: Box<dyn std::error::Error> = Box::new(DbError::QueryTimeout(5.0));
assert!(e.to_string().contains("5.0"));
}
#[test]
fn test_timeout_format() {
let e = DbError::QueryTimeout(30.567);
assert!(e.to_string().contains("30.6")); // formatted to 1 decimal
}
}Key Differences
std::error::Error requires both Display and Debug; OCaml has no such requirement — any type can be an error.#[derive(Debug)] generates a complete structural representation; implementing it manually is rarely needed.eprintln!("Error: {}", e) shows user message; eprintln!("Debug: {:?}", e) shows developer details.assert_eq!(format!("{}", err), "expected user message") to test Display; use {:?} for debugging assertions.OCaml Approach
OCaml distinguishes pp (pretty-printer for structured output) from to_string (human-readable). ppx_sexp_conv auto-derives structured output similar to #[derive(Debug)]:
(* ppx_sexp_conv generates structured output like Debug *)
[@@deriving sexp_of]
(* Manual display function like Display *)
let to_user_string = function
| ConnectionFailed h -> Printf.sprintf "Cannot connect to %s" h
| QueryTimeout s -> Printf.sprintf "Query timed out after %.1fs" s
Full Source
#![allow(clippy::all)]
//! # Error Display vs Debug
//!
//! Display is for users, Debug is for developers.
use std::fmt;
#[derive(Debug)]
pub enum DbError {
ConnectionFailed(String),
QueryTimeout(f64),
NotFound(String),
}
impl fmt::Display for DbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ConnectionFailed(h) => write!(f, "Cannot connect to {h}"),
Self::QueryTimeout(s) => write!(f, "Query timed out after {s:.1}s"),
Self::NotFound(k) => write!(f, "Record not found: {k}"),
}
}
}
impl std::error::Error for DbError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_human_readable() {
let e = DbError::ConnectionFailed("localhost".into());
assert_eq!(e.to_string(), "Cannot connect to localhost");
}
#[test]
fn test_debug_has_variant() {
let e = DbError::NotFound("x".into());
let debug = format!("{:?}", e);
assert!(debug.contains("NotFound"));
}
#[test]
fn test_implements_error() {
let e: Box<dyn std::error::Error> = Box::new(DbError::QueryTimeout(5.0));
assert!(e.to_string().contains("5.0"));
}
#[test]
fn test_timeout_format() {
let e = DbError::QueryTimeout(30.567);
assert!(e.to_string().contains("30.6")); // formatted to 1 decimal
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_human_readable() {
let e = DbError::ConnectionFailed("localhost".into());
assert_eq!(e.to_string(), "Cannot connect to localhost");
}
#[test]
fn test_debug_has_variant() {
let e = DbError::NotFound("x".into());
let debug = format!("{:?}", e);
assert!(debug.contains("NotFound"));
}
#[test]
fn test_implements_error() {
let e: Box<dyn std::error::Error> = Box::new(DbError::QueryTimeout(5.0));
assert!(e.to_string().contains("5.0"));
}
#[test]
fn test_timeout_format() {
let e = DbError::QueryTimeout(30.567);
assert!(e.to_string().contains("30.6")); // formatted to 1 decimal
}
}
Deep Comparison
error-display-debug
See README.md for details.
Exercises
Display for an error type that produces a one-line user message, and verify that format!("{}", err) produces the expected output.format!("{}", err) and format!("{:?}", err) output for the same DbError value.Display and Debug outputs meet their respective format expectations for all variants.