ExamplesBy LevelBy TopicLearning Paths
318 Intermediate

318: Display vs Debug for Errors

Functional Programming

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

  • • Understand Display as user-facing output (error messages shown to end users)
  • • Understand Debug as developer-facing output (detailed diagnostic information)
  • • Implement both traits on the same error type for different audiences
  • • Recognize that {:?} uses Debug, {} uses Display in format strings
  • Code 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

  • Two required traits: Rust's std::error::Error requires both Display and Debug; OCaml has no such requirement — any type can be an error.
  • Auto-derive Debug: #[derive(Debug)] generates a complete structural representation; implementing it manually is rarely needed.
  • Error propagation format: eprintln!("Error: {}", e) shows user message; eprintln!("Debug: {:?}", e) shows developer details.
  • Testing: Use 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
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Implement Display for an error type that produces a one-line user message, and verify that format!("{}", err) produces the expected output.
  • Show the difference between format!("{}", err) and format!("{:?}", err) output for the same DbError value.
  • Write a test that verifies both Display and Debug outputs meet their respective format expectations for all variants.
  • Open Source Repos