scoped_error/error.rs
1// Copyright (C) 2026 Kan-Ru Chen <kanru@kanru.info>
2//
3// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5//! Core error types and traits for context-aware error handling.
6//!
7//! This module defines [`WithContext`], the central trait that enables
8//! error context propagation, along with [`scoped_error::Error`] (a ready-to-use error
9//! type) and [`Frame`] (a single layer of context).
10
11use std::any::Any;
12use std::borrow::Cow;
13use std::error::Error as StdError;
14use std::fmt::{Debug, Display};
15use std::panic::Location;
16
17use crate::ext::ErrorExt;
18
19/// A trait for error types that can carry context information.
20///
21/// Types implementing this trait can have context (message + location)
22/// attached to them via [`with_context`](Self::with_context). This is the
23/// core abstraction enabling the library's context propagation.
24///
25/// # Implementing
26///
27/// For most use cases, use the [`impl_context_error!`](crate::impl_context_error)
28/// macro instead of implementing this manually.
29///
30/// # Example
31///
32/// ```
33/// use std::borrow::Cow;
34/// use std::error::Error;
35/// use std::fmt::{Debug, Display};
36/// use std::panic::Location;
37///
38/// use scoped_error::Frame;
39/// use scoped_error::WithContext;
40///
41/// // Custom error type with context support
42/// #[derive(Debug)]
43/// struct MyError {
44/// message: Cow<'static, str>,
45/// source: Option<Box<dyn Error + Send + Sync>>,
46/// location: Option<&'static Location<'static>>,
47/// }
48///
49/// impl Error for MyError {}
50/// impl Display for MyError {
51/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52/// if let Some(loc) = self.location {
53/// write!(f, "{}, at {}", self.message, loc)
54/// } else {
55/// f.write_str(&self.message)
56/// }
57/// }
58/// }
59///
60/// impl WithContext for MyError {
61/// fn with_context(mut self, context: Frame) -> Self {
62/// self.source = Some(context.source);
63/// self.location = Some(context.location);
64/// self
65/// }
66///
67/// fn location(&self) -> Option<&'static Location<'static>> {
68/// self.location
69/// }
70/// }
71/// ```
72pub trait WithContext: StdError + Any {
73 /// Attach a context layer to this error.
74 ///
75 /// The context includes a source error and the location where the
76 /// context was added (captured via `#[track_caller]`).
77 fn with_context(self, context: Frame) -> Self;
78
79 /// Get the location where this error was created or where context was attached.
80 fn location(&self) -> Option<&'static Location<'static>>;
81}
82
83/// A single layer of error context.
84///
85/// Contains the source error that caused this context layer, along with
86/// the location where the context was added. Created automatically by
87/// the conversion functions when an error occurs.
88///
89/// # Creation
90///
91/// `Frame` is typically created via `From<T>` where `T` can be
92/// converted to `Box<dyn Error + Send + Sync>`. The location is captured
93/// using `#[track_caller]`.
94pub struct Frame {
95 /// The underlying error that caused this context.
96 pub source: Box<dyn StdError + Send + Sync + 'static>,
97 /// The location where this context was attached.
98 pub location: &'static Location<'static>,
99}
100
101impl<T> From<T> for Frame
102where
103 T: Into<Box<dyn StdError + Send + Sync + 'static>>,
104{
105 /// Creates an `Frame` from any error type, capturing the caller's location.
106 #[track_caller]
107 fn from(value: T) -> Self {
108 let source = value.into();
109 let location = Location::caller();
110 Frame { source, location }
111 }
112}
113
114/// A convenient, ready-to-use error type with built-in context support.
115///
116/// [`Error`] can be used directly without defining custom error types.
117/// It stores an error message, optional source error, and optional location.
118///
119/// # Example
120///
121/// ```
122/// use scoped_error::Error;
123///
124/// fn fallible() -> Result<(), Error> {
125/// // &str or String can be converted to Error
126/// Err(Error::new("Something went wrong"))
127/// }
128/// ```
129pub struct Error {
130 /// The error message.
131 pub message: Cow<'static, str>,
132 /// The underlying error that caused this one.
133 pub source: Option<Box<dyn StdError + Send + Sync + 'static>>,
134 /// The location where this error was created.
135 pub location: Option<&'static Location<'static>>,
136}
137
138impl Error {
139 /// Create a new `Error` with the given message.
140 ///
141 /// The source and location are initially `None`.
142 pub fn new(msg: impl Into<Cow<'static, str>>) -> Error {
143 Error {
144 message: msg.into(),
145 source: None,
146 location: None,
147 }
148 }
149}
150
151impl Debug for Error {
152 /// Formats using the error report for human-readable output.
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 self.report().fmt(f)
155 }
156}
157
158impl Display for Error {
159 /// Displays the message with location if available.
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 if let Some(loc) = self.location {
162 write!(f, "{}, at {}", self.message, loc)
163 } else {
164 f.write_str(&self.message)
165 }
166 }
167}
168
169impl StdError for Error {
170 fn source(&self) -> Option<&(dyn StdError + 'static)> {
171 self.source.as_deref().map(|s| s as _)
172 }
173}
174
175impl WithContext for Error {
176 fn with_context(mut self, context: Frame) -> Self {
177 self.source = Some(context.source);
178 self.location = Some(context.location);
179 self
180 }
181 fn location(&self) -> Option<&'static Location<'static>> {
182 self.location
183 }
184}
185
186impl From<(Cow<'static, str>, Frame)> for Error {
187 fn from(value: (Cow<'static, str>, Frame)) -> Self {
188 Error {
189 message: value.0,
190 source: Some(value.1.source),
191 location: Some(value.1.location),
192 }
193 }
194}