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::msgSynthesis
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 callsKey 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.
