How does anyhow::Error::new differ from msg for creating errors from scratch?

anyhow::Error::new wraps an existing error type that implements std::error::Error, while msg creates an error directly from a string message without requiring an underlying error type. The distinction matters for error semantics: new preserves an existing error's type and chain, while msg creates a new ad-hoc error. Choosing between them depends on whether you're wrapping an existing error or creating one from scratch.

Creating Errors with msg

use anyhow::{Error, bail};
 
fn creating_with_msg() -> Result<(), Error> {
    // msg creates an error from a string-like value
    // It's the simplest way to create an error from scratch
    
    // Direct construction:
    let error = Error::msg("something went wrong");
    println!("Error: {}", error);
    
    // From String:
    let message = String::from("dynamic error message");
    let error = Error::msg(message);
    
    // From &str:
    let error = Error::msg("static string");
    
    // Using bail! macro (uses msg internally):
    bail!("operation failed");
    
    // With format! style:
    let value = 42;
    bail!("value {} is invalid", value);
    
    Ok(())
}

msg creates a new error with just a messageβ€”no underlying error type is wrapped.

Creating Errors with new

use anyhow::Error;
use std::io;
 
fn creating_with_new() -> Result<(), Error> {
    // new wraps an existing error type
    
    // Wrapping an io::Error:
    let io_error = io::Error::new(io::ErrorKind::NotFound, "file missing");
    let error = Error::new(io_error);
    
    // Error chain shows the underlying error:
    println!("Error: {}", error);
    // Prints: file missing
    
    // From a Result:
    let result: Result<String, io::Error> = Err(io::Error::new(
        io::ErrorKind::PermissionDenied,
        "access denied"
    ));
    let error: Error = result.unwrap_err().into();
    // Error::new is called via .into()
    
    Ok(())
}
 
// new requires something that implements std::error::Error
#[derive(Debug)]
struct CustomError {
    code: i32,
}
 
impl std::fmt::Display for CustomError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "error code: {}", self.code)
    }
}
 
impl std::error::Error for CustomError {}
 
fn custom_error_new() -> Result<(), Error> {
    let custom = CustomError { code: 42 };
    let error = Error::new(custom);
    // The CustomError is wrapped and its Display is used
    
    Err(error)
}

new wraps types implementing std::error::Error, preserving the original error's properties.

The Type Signature Difference

use anyhow::Error;
use std::error::Error as StdError;
 
// Type signatures reveal the difference:
 
impl Error {
    // msg: Creates from any Display type
    pub fn msg<M: std::fmt::Display + Send + Sync + 'static>(msg: M) -> Error
    // Takes anything that can be displayed as a string
    
    // new: Wraps an existing error
    pub fn new<E: StdError + Send + Sync + 'static>(error: E) -> Error
    // Takes something implementing std::error::Error
}
 
fn signature_examples() {
    // msg accepts anything Display:
    Error::msg("string literal");
    Error::msg(String::from("owned string"));
    Error::msg(format!("formatted {}", "message"));
    Error::msg(42);  // Even numbers work (they implement Display)
    
    // new requires std::error::Error:
    use std::io;
    Error::new(io::Error::new(io::ErrorKind::Other, "io error"));
    
    // This won't compile - String doesn't implement Error:
    // Error::new("string");  // Error: String doesn't implement Error
    
    // This won't compile - &str doesn't implement Error:
    // Error::new("literal");  // Error: &str doesn't implement Error
}

The signature difference: msg takes Display, new takes Error.

When to Use Each

use anyhow::{Error, bail, Context};
 
// Use msg (or bail!) when:
// 1. Creating an error from scratch
// 2. No underlying error type exists
// 3. The error is truly a "message" error
 
fn use_msg_examples() -> Result<(), Error> {
    // Validation failures:
    let value = 150;
    if value > 100 {
        bail!("value {} exceeds maximum of 100", value);
    }
    
    // Business rule violations:
    let balance = 50;
    let withdrawal = 100;
    if withdrawal > balance {
        bail!("insufficient funds: balance {}, requested {}", balance, withdrawal);
    }
    
    // Configuration errors:
    let config_value: Option<i32> = None;
    let value = config_value.ok_or_else(|| Error::msg("config missing required value"))?;
    
    // State violations:
    let is_initialized = false;
    if !is_initialized {
        bail!("service not initialized");
    }
    
    Ok(())
}
 
// Use new when:
// 1. Wrapping an existing error type
// 2. Converting from another error crate
// 3. Preserving the error chain
 
fn use_new_examples() -> Result<(), Error> {
    use std::io;
    
    // Wrapping IO errors:
    let file_content = std::fs::read_to_string("config.txt")
        .map_err(Error::new)?;
    
    // Equivalent using ? and Into:
    let file_content = std::fs::read_to_string("config.txt")?;
    // The ? automatically converts io::Error -> anyhow::Error via Into
    // This calls Error::new internally
    
    // Wrapping custom errors:
    #[derive(Debug)]
    struct DatabaseError {
        message: String,
    }
    
    impl std::fmt::Display for DatabaseError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{}", self.message)
        }
    }
    
    impl std::error::Error for DatabaseError {}
    
    let db_error = DatabaseError {
        message: "connection failed".to_string(),
    };
    let error = Error::new(db_error);
    
    Ok(())
}

Use msg for message-only errors, new for wrapping existing error types.

Error Chain Behavior

use anyhow::Error;
 
fn error_chain_demo() {
    use std::io;
    
    // msg: Single error, no chain
    let msg_error = Error::msg("something failed");
    println!("msg error: {}", msg_error);
    // Prints: something failed
    // No cause chain
    
    // new: Preserves the error type
    let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
    let new_error = Error::new(io_error);
    println!("new error: {}", new_error);
    // Prints: file not found
    
    // The underlying error is still accessible:
    // (Requires downcasting, covered later)
    
    // Chain with context:
    let chained = std::fs::read_to_string("missing.txt")
        .map_err(|e| Error::new(e).context("failed to read config"))?;
    
    // This creates a chain:
    // - "failed to read config" (context)
    // - "file not found" (underlying io::Error)
}
 
fn chain_source() {
    use std::io;
    
    // msg creates an error without a source
    let msg_error = Error::msg("simple error");
    assert!(msg_error.source().is_none());
    
    // new preserves the source
    let io_error = io::Error::new(io::ErrorKind::Other, "underlying");
    let new_error = Error::new(io_error);
    assert!(new_error.source().is_some());
    // The source is the original io::Error
}

msg creates standalone errors; new preserves the error chain from wrapped errors.

Converting Between Approaches

use anyhow::{Error, Context};
 
// Common pattern: Convert errors and add context
 
fn conversion_patterns() -> Result<(), Error> {
    use std::io;
    
    // Automatic conversion via ? operator:
    // This calls Error::new internally via From/Into
    let content = std::fs::read_to_string("file.txt")?;
    // io::Error is converted to anyhow::Error using Error::new
    
    // With context (preferred):
    let content = std::fs::read_to_string("file.txt")
        .context("failed to read file.txt")?;
    // context() wraps the error and adds a message layer
    
    // Manual conversion:
    let io_result: Result<String, io::Error> = Err(io::Error::new(
        io::ErrorKind::PermissionDenied,
        "access denied"
    ));
    
    // Using new directly:
    let error = io_result.map_err(Error::new)?;
    
    // Using context:
    let error = io_result.context("while reading config")?;
    
    // msg vs new for custom errors:
    
    // Using msg (just the message):
    let error1 = Error::msg("database connection failed");
    
    // Using new (wrapping a typed error):
    #[derive(Debug)]
    struct DbError(&'static str);
    impl std::fmt::Display for DbError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{}", self.0)
        }
    }
    impl std::error::Error for DbError {}
    
    let error2 = Error::new(DbError("database connection failed"));
    
    // error1 has no source, error2 has DbError as source
}

The ? operator automatically converts errors using Error::new via the From trait.

Practical Examples

use anyhow::{Error, bail, Context, Result};
 
// Example 1: Input validation (use msg/bail)
fn validate_age(age: i32) -> Result<()> {
    if age < 0 {
        bail!("age cannot be negative: {}", age);
    }
    if age > 150 {
        bail!("age seems unrealistic: {}", age);
    }
    Ok(())
}
 
// Example 2: File operations (use new via ? or context)
fn read_config(path: &str) -> Result<String> {
    // The ? converts io::Error to anyhow::Error using new
    std::fs::read_to_string(path)
        .context(format!("failed to read config from {}", path))
}
 
// Example 3: Combining both
fn load_user_config(user_id: u32) -> Result<Config> {
    // Validation uses msg-style
    if user_id == 0 {
        bail!("user_id cannot be zero");
    }
    
    // File operations wrap existing errors
    let path = format!("config/user_{}.json", user_id);
    let content = std::fs::read_to_string(&path)
        .context(format!("failed to load config for user {}", user_id))?;
    
    // Parsing creates its own error type
    let config: Config = serde_json::from_str(&content)
        .context("invalid config format")?;
    
    // Validation again
    if config.name.is_empty() {
        bail!("config missing required field: name");
    }
    
    Ok(config)
}
 
#[derive(Debug)]
struct Config {
    name: String,
}
 
// Example 4: Custom error types
#[derive(Debug)]
pub enum AppError {
    InvalidInput(String),
    NotFound(String),
    PermissionDenied(String),
}
 
impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AppError::InvalidInput(msg) => write!(f, "invalid input: {}", msg),
            AppError::NotFound(msg) => write!(f, "not found: {}", msg),
            AppError::PermissionDenied(msg) => write!(f, "permission denied: {}", msg),
        }
    }
}
 
impl std::error::Error for AppError {}
 
fn use_custom_error() -> Result<()> {
    // Convert custom error to anyhow::Error using new
    let error = AppError::InvalidInput("email is required".to_string());
    Err(Error::new(error))
    
    // Or use ? operator with Into implementation
    // Err(AppError::NotFound("user".to_string()))?;
}

Real code combines both: msg/bail! for validation errors, new (via ?) for wrapping external errors.

Downcasting Differences

use anyhow::Error;
 
fn downcast_demo() {
    use std::io;
    
    // Errors created with msg cannot be downcast to specific types
    // (unless they're downcast to &str or String)
    let msg_error = Error::msg("error message");
    
    // Can downcast to &str:
    if let Some(message) = msg_error.downcast_ref::<&str>() {
        println!("Message: {}", message);
    }
    
    // Cannot downcast to std::io::Error:
    assert!(msg_error.downcast_ref::<io::Error>().is_none());
    
    // Errors created with new preserve the type:
    let io_error = io::Error::new(io::ErrorKind::NotFound, "file missing");
    let new_error = Error::new(io_error);
    
    // Can downcast to the original type:
    if let Some(io_err) = new_error.downcast_ref::<io::Error>() {
        println!("IO Error kind: {:?}", io_err.kind());
    }
    
    // Can also get as the Error trait object:
    if let Some(err) = new_error.downcast_ref::<dyn std::error::Error>() {
        println!("Error: {}", err);
    }
}

new-created errors preserve type information for downcasting; msg errors are message-only.

Comparison Summary

use anyhow::Error;
 
fn comparison_table() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Aspect             β”‚ msg vs new                                      β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ Input type         β”‚ msg: Display, new: std::error::Error           β”‚
    // β”‚ Creates new error  β”‚ msg: yes, new: wraps existing                  β”‚
    // β”‚ Error source       β”‚ msg: none, new: preserved                      β”‚
    // β”‚ Downcasting        β”‚ msg: to string only, new: to original type     β”‚
    // β”‚ Use case           β”‚ msg: ad-hoc errors, new: wrapping              β”‚
    // β”‚ Typical usage      β”‚ msg: bail!/msg!, new: ? or .into()             β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
}
 
// Practical decision tree:
// 
// Q: Do I have an existing error type?
//     YES β†’ Use Error::new (or ? with From/Into)
//     NO  β†’ Continue
// 
// Q: Is this a simple message error?
//     YES β†’ Use Error::msg or bail!
//     NO  β†’ Continue
// 
// Q: Do I need custom error type?
//     YES β†’ Implement std::error::Error, use Error::new
//     NO  β†’ Use bail! or Error::msg

Synthesis

Quick reference:

use anyhow::{Error, bail, Result};
 
// msg: Create error from message string
let error = Error::msg("something failed");
let error = Error::msg(format!("value {} is invalid", 42));
bail!("operation failed");  // Uses msg internally
 
// new: Wrap an existing error type
use std::io;
let io_error = io::Error::new(io::ErrorKind::NotFound, "file");
let error = Error::new(io_error);
 
// When to use each:
// - Validation errors β†’ bail! or Error::msg
// - Business rule violations β†’ bail! or Error::msg  
// - Wrapping io::Error β†’ ? (uses Error::new via From)
// - Wrapping serde_json::Error β†’ ? (uses Error::new)
// - Custom error types β†’ Error::new(your_error)
 
// Common patterns:
// Pattern 1: Validation with bail!
fn validate(input: &str) -> Result<()> {
    if input.is_empty() {
        bail!("input cannot be empty");
    }
    Ok(())
}
 
// Pattern 2: File operations with context
fn read_file(path: &str) -> Result<String> {
    std::fs::read_to_string(path)
        .context(format!("failed to read {}", path))
}
 
// Pattern 3: Custom error types
impl std::error::Error for MyError {}
fn my_function() -> Result<()> {
    Err(Error::new(MyError::InvalidState))
}
 
// Anti-patterns:
// ❌ Error::new("message") // Doesn't compile: &str isn't Error
// ❌ Error::msg(io_error)   // Works but loses type info
// ❌ Creating custom types just for msg (use bail! instead)
 
// Best practices:
// βœ… Use bail! for message-only errors
// βœ… Use ? for wrapping existing errors
// βœ… Use .context() to add context to wrapped errors
// βœ… Create proper error types for complex errors
// βœ… Prefer ? over manual Error::new calls

Key insight: Error::msg and Error::new serve different purposes in the anyhow ecosystem. msg creates ad-hoc errors from displayable contentβ€”use it for validation failures, business rule violations, and other cases where you need to signal failure with just a message. new wraps existing std::error::Error typesβ€”use it (or the ? operator which calls it implicitly) when propagating errors from other libraries like std::io or serde. The choice affects error handling downstream: new-wrapped errors preserve their type for potential downcasting and maintain the error source chain, while msg-created errors are standalone messages. In practice, use bail! (which uses msg internally) for simple message errors and ? with .context() for wrapping and enriching existing errors.