Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ path = "src/downcast.rs"
[package.metadata.release]
release = false

[dependencies]
[dev-dependencies]
derive_more = { workspace = true }
exn = { workspace = true }

Expand Down
11 changes: 6 additions & 5 deletions examples/src/custom-layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! This example shows how to traverse the error chain and create custom
//! formatting to match your application's needs.

use std::error::Error;
use std::fmt::Write;

use exn::Exn;
Expand Down Expand Up @@ -48,18 +49,18 @@ impl std::error::Error for MainError {}

impl MainError {
/// Convert an `Exn<E>` into MainError with custom numbered list formatting.
pub fn new<E: exn::Error>(err: Exn<E>) -> Self {
pub fn new<E: Error>(err: Exn<E>) -> Self {
fn collect_frames(frame: &Frame, frames: &mut Vec<String>) {
// Add this frame first
frames.push(format!("[{}] {}", frame.location(), frame.as_error()));
frames.push(format!("[{}] {}", frame.location(), frame.error()));
// Then collect children
for child in frame.children() {
collect_frames(child, frames);
}
}

let mut frames = vec![];
collect_frames(err.as_frame(), &mut frames);
collect_frames(err.frame(), &mut frames);

// Format as numbered list
let mut report = String::new();
Expand Down Expand Up @@ -120,5 +121,5 @@ mod http {
// Output when running `cargo run --example custom_layout`:
//
// Error: fatal error occurred in application:
// 0: [examples/src/custom-layout.rs:81:30] failed to run app
// 1: [examples/src/custom-layout.rs:101:9] failed to send request to server: https://example.com
// 0: [examples/src/custom-layout.rs:82:30] failed to run app
// 1: [examples/src/custom-layout.rs:102:9] failed to send request to server: https://example.com
14 changes: 8 additions & 6 deletions examples/src/downcast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
//! - Recover from specific error types
//! - Extract structured data (HTTP codes, retry hints, etc.)

use std::error::Error;

use derive_more::Display;
use exn::Exn;
use exn::Frame;
Expand Down Expand Up @@ -54,18 +56,18 @@ fn main() -> Result<(), MainError> {
}

/// Walk the error chain and extract HTTP status code if present.
fn extract_http_status<E: exn::Error>(err: &Exn<E>) -> Option<u16> {
fn extract_http_status<E: Error>(err: &Exn<E>) -> Option<u16> {
fn walk(frame: &Frame) -> Option<u16> {
// Try to downcast current frame
if let Some(http_err) = frame.as_any().downcast_ref::<HttpError>() {
if let Some(http_err) = frame.error().downcast_ref::<HttpError>() {
return Some(http_err.status);
}

// Check children recursively
frame.children().iter().find_map(walk)
}

walk(err.as_frame())
walk(err.frame())
}

#[derive(Debug, Display)]
Expand Down Expand Up @@ -117,8 +119,8 @@ mod http {
// Retryable error, attempting retry #3
//
// HTTP error with status code: 503
// Error: fatal error occurred in application, at examples/src/downcast.rs:52:24
// Error: fatal error occurred in application, at examples/src/downcast.rs:54:24
// |
// |-> failed to run app, at examples/src/downcast.rs:80:35
// |-> failed to run app, at examples/src/downcast.rs:82:35
// |
// |-> HTTP 503: service unavailable, at examples/src/downcast.rs:93:9
// |-> HTTP 503: service unavailable, at examples/src/downcast.rs:95:9
2 changes: 0 additions & 2 deletions exn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ repository.workspace = true
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]

[dev-dependencies]
insta = { workspace = true }

Expand Down
6 changes: 3 additions & 3 deletions exn/src/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::error::Error;
use std::fmt;
use std::fmt::Formatter;

use crate::Error;
use crate::Exn;
use crate::Frame;

impl<E: Error> fmt::Debug for Exn<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write_exn(f, self.as_frame(), 0, "")
write_exn(f, self.frame(), 0, "")
}
}

Expand All @@ -32,7 +32,7 @@ impl fmt::Debug for Frame {
}

fn write_exn(f: &mut Formatter<'_>, frame: &Frame, level: usize, prefix: &str) -> fmt::Result {
write!(f, "{}", frame.as_error())?;
write!(f, "{}", frame.error())?;
write_location(f, frame)?;

let children = frame.children();
Expand Down
4 changes: 2 additions & 2 deletions exn/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::error::Error;
use std::fmt;

use crate::Error;
use crate::Exn;

impl<E: Error> fmt::Display for Exn<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_error())
write!(f, "{}", self.error())
}
}
36 changes: 14 additions & 22 deletions exn/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::error::Error;
use std::fmt;
use std::marker::PhantomData;
use std::panic::Location;

use crate::Error;

/// An exception type that can hold an error tree and additional context.
pub struct Exn<E: Error> {
pub struct Exn<E: Error + 'static> {
// trade one more indirection for less stack size
frame: Box<Frame>,
phantom: PhantomData<E>,
}

impl<E: Error> From<E> for Exn<E> {
impl<E: Error + 'static> From<E> for Exn<E> {
#[track_caller]
fn from(error: E) -> Self {
Exn::new(error)
}
}

impl<E: Error> Exn<E> {
impl<E: Error + 'static> Exn<E> {
/// Create a new exception with the given error.
///
/// This will automatically walk the [source chain of the error] and add them as children
/// frames.
///
/// See also [`Error::raise`] for a fluent way to convert an error into an `Exn` instance.
///
/// [source chain of the error]: std::error::Error::source
/// [source chain of the error]: Error::source
#[track_caller]
pub fn new(error: E) -> Self {
struct SourceError(String);
Expand All @@ -57,9 +54,9 @@ impl<E: Error> Exn<E> {
}
}

impl std::error::Error for SourceError {}
impl Error for SourceError {}

fn walk(error: &dyn std::error::Error, location: &'static Location<'static>) -> Vec<Frame> {
fn walk(error: &dyn Error, location: &'static Location<'static>) -> Vec<Frame> {
if let Some(source) = error.source() {
let children = vec![Frame {
error: Box::new(SourceError(source.to_string())),
Expand Down Expand Up @@ -90,7 +87,7 @@ impl<E: Error> Exn<E> {
#[track_caller]
pub fn from_iter<T, I>(children: I, err: E) -> Self
where
T: Error,
T: Error + 'static,
I: IntoIterator,
I::Item: Into<Exn<T>>,
{
Expand All @@ -111,37 +108,32 @@ impl<E: Error> Exn<E> {
}

/// Return the current exception.
pub fn as_error(&self) -> &E {
pub fn error(&self) -> &E {
self.frame
.as_any()
.error()
.downcast_ref()
.expect("error type must match")
}

/// Return the underlying exception frame.
pub fn as_frame(&self) -> &Frame {
pub fn frame(&self) -> &Frame {
&self.frame
}
}

/// A frame in the exception tree.
pub struct Frame {
/// The error that occurred at this frame.
error: Box<dyn Error>,
error: Box<dyn Error + 'static>,
/// The source code location where this exception frame was created.
location: &'static Location<'static>,
/// Child exception frames that provide additional context or source errors.
children: Vec<Frame>,
}

impl Frame {
/// Return the error as a reference to [`std::any::Any`].
pub fn as_any(&self) -> &dyn std::any::Any {
&*self.error
}

/// Return the error as a reference to [`std::error::Error`].
pub fn as_error(&self) -> &dyn std::error::Error {
/// Return the error that occurred at this frame.
pub fn error(&self) -> &(dyn Error + 'static) {
&*self.error
}

Expand Down
16 changes: 1 addition & 15 deletions exn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,6 @@ pub use self::option::OptionExt;
pub use self::result::Result;
pub use self::result::ResultExt;

/// A trait bound of the supported error type of [`Exn`].
pub trait Error: std::error::Error + std::any::Any + Send + Sync + 'static {
/// Raise this error as a new exception.
#[track_caller]
fn raise(self) -> Exn<Self>
where
Self: Sized,
{
Exn::new(self)
}
Comment thread
tisonkun marked this conversation as resolved.
}

impl<T> Error for T where T: std::error::Error + std::any::Any + Send + Sync + 'static {}

/// Equivalent to `Ok::<_, Exn<E>>(value)`.
///
/// This simplifies creation of an `exn::Result` in places where type inference cannot deduce the
Expand All @@ -119,6 +105,6 @@ impl<T> Error for T where T: std::error::Error + std::any::Any + Send + Sync + '
/// | consider giving this pattern the explicit type `std::result::Result<i32, E>`, where the type parameter `E` is specified
/// ```
#[expect(non_snake_case)]
pub fn Ok<T, E: Error>(value: T) -> Result<T, E> {
pub fn Ok<T, E: std::error::Error + 'static>(value: T) -> Result<T, E> {

@andylokandy andylokandy Jan 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use std::error::Error to keep consistent?

@tisonkun tisonkun Jan 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in the lib.rs, so I'd make an exception to avoid use crate::Error, an alias to std::error:Error, which can cause more ambiguity.

Perhaps we can move this function to a new file and pub use it at lib.rs, while use std::error::Error in that file. But let's do it later?

Result::Ok(value)
}
2 changes: 1 addition & 1 deletion exn/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
/// use std::fs;
///
/// use exn::bail;
/// # fn wrapper() -> exn::Result<(), impl exn::Error> {
/// # fn wrapper() -> exn::Result<(), std::io::Error> {
/// match fs::read_to_string("/path/to/file") {
/// Ok(content) => println!("file contents: {content}"),
/// Err(err) => bail!(err),
Expand Down
3 changes: 2 additions & 1 deletion exn/src/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::Error;
use std::error::Error;

use crate::Exn;
use crate::Result;

Expand Down
11 changes: 6 additions & 5 deletions exn/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::Error;
use std::error::Error;

use crate::Exn;

/// A reasonable return type to use throughout an application.
Expand All @@ -24,7 +25,7 @@ pub trait ResultExt {
type Success;

/// The `Err` type that would be wrapped in an [`Exn`].
type Error: Error;
type Error: Error + 'static;
Comment thread
tisonkun marked this conversation as resolved.

/// Raise a new exception on the [`Exn`] inside the [`Result`].
///
Expand All @@ -37,15 +38,15 @@ pub trait ResultExt {

impl<T, E> ResultExt for std::result::Result<T, E>
where
E: Error,
E: Error + 'static,
{
type Success = T;
type Error = E;

#[track_caller]
fn or_raise<A, F>(self, err: F) -> Result<Self::Success, A>
where
A: Error,
A: Error + 'static,
F: FnOnce() -> A,
{
match self {
Expand All @@ -65,7 +66,7 @@ where
#[track_caller]
fn or_raise<A, F>(self, err: F) -> Result<Self::Success, A>
where
A: Error,
A: Error + 'static,
F: FnOnce() -> A,
{
match self {
Expand Down
13 changes: 6 additions & 7 deletions exn/tests/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use exn::Error;
use exn::Exn;
use exn::OptionExt;
use exn::ResultExt;
Expand All @@ -30,7 +29,7 @@ impl std::error::Error for SimpleError {}

#[test]
fn test_error_straightforward() {
let e1 = SimpleError("E1").raise();
let e1 = Exn::new(SimpleError("E1"));
let e2 = e1.raise(SimpleError("E2"));
let e3 = e2.raise(SimpleError("E3"));
let e4 = e3.raise(SimpleError("E4"));
Expand All @@ -40,21 +39,21 @@ fn test_error_straightforward() {

#[test]
fn test_error_tree() {
let e1 = SimpleError("E1").raise();
let e1 = Exn::new(SimpleError("E1"));
let e3 = e1.raise(SimpleError("E3"));

let e9 = SimpleError("E9").raise();
let e9 = Exn::new(SimpleError("E9"));
let e10 = e9.raise(SimpleError("E10"));

let e11 = SimpleError("E11").raise();
let e11 = Exn::new(SimpleError("E11"));
let e12 = e11.raise(SimpleError("E12"));

let e5 = Exn::from_iter([e3, e10, e12], SimpleError("E5"));

let e2 = SimpleError("E2").raise();
let e2 = Exn::new(SimpleError("E2"));
let e4 = e2.raise(SimpleError("E4"));

let e7 = SimpleError("E7").raise();
let e7 = Exn::new(SimpleError("E7"));
let e8 = e7.raise(SimpleError("E8"));

let e6 = Exn::from_iter([e5, e4, e8], SimpleError("E6"));
Expand Down
2 changes: 1 addition & 1 deletion exn/tests/snapshots/simple__bail.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
source: exn/tests/simple.rs
expression: result.unwrap_err()
---
An error, at exn/tests/simple.rs:92:9
An error, at exn/tests/simple.rs:91:9
2 changes: 1 addition & 1 deletion exn/tests/snapshots/simple__ensure_fail.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
source: exn/tests/simple.rs
expression: result.unwrap_err()
---
An error, at exn/tests/simple.rs:112:9
An error, at exn/tests/simple.rs:111:9
Loading