Skip to content
Draft
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
4 changes: 4 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ path = "src/downcast.rs"
name = "make-error"
path = "src/make-error.rs"

[[example]]
name = "recovery"
path = "src/recovery.rs"

[[example]]
name = "into-anyhow"
path = "src/into-anyhow.rs"
Expand Down
126 changes: 126 additions & 0 deletions examples/src/recovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2025 FastLabs Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! # Recovery Example
//!
//! This example shows how to get back non-clonable objects on failure.

use std::error::Error;

use exn::Exn;
use exn::ResultExt;

fn main() -> Result<(), Exn<MainError>> {
let err = || MainError("fatal error occurred in application".to_string());
let body = app::load_site().or_raise(err)?;
println!("{}", body.0);
Ok(())
}

#[derive(Debug)]
struct MainError(String);

impl std::fmt::Display for MainError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

impl Error for MainError {}

mod app {
use super::*;

pub fn load_site() -> Result<http::Resource, Exn<AppError>> {
// Here we create a non-clonable resource as it should be handled only once.
let request = http::Request::new();

let cache_exn = match request.load_from_cache() {
Ok(resource) => return Ok(resource),
Err(exn) => exn,
};
// Recover from the error to try a different approach.
let (request, cache_exn) = cache_exn.recover();

let server_exn = match request.send_request("https://example.com") {
Ok(resource) => return Ok(resource),
Err(exn) => exn,
};
// When we have no other way to recover, we just discard it.
let server_exn = server_exn.discard_recovery();

let msg = "failed to run app".to_string();
let exn = Exn::raise_all(AppError(msg), vec![cache_exn, server_exn]);
Err(exn)
}

#[derive(Debug)]
pub struct AppError(String);

impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

impl Error for AppError {}
}

mod http {
use super::*;

pub struct Request(String);

impl Request {
pub fn new() -> Self {
Self("index.html".to_string())
}

pub fn load_from_cache(self) -> Result<Resource, Exn<HttpError, Self>> {
let msg = format!("failed to load from cache: {:?}", self.0);
let exn = Exn::with_recovery(HttpError(msg), self);
Err(exn)
}

pub fn send_request(self, server: &str) -> Result<Resource, Exn<HttpError, Self>> {
let request = format!("{server}/{}", self.0);
let msg = format!("request to server failed: {request:?}");
let exn = Exn::with_recovery(HttpError(msg), self);
Err(exn)
}
}

pub struct Resource(pub String);

#[derive(Debug)]
pub struct HttpError(String);

impl std::fmt::Display for HttpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

impl Error for HttpError {}
}

// Output when running `cargo run --example recovery`:
//
// Error: fatal error occurred in application, at examples/src/recovery.rs:26:33
// |
// |-> failed to run app, at examples/src/recovery.rs:64:19
// |
// |-> failed to load from cache: "index.html", at examples/src/recovery.rs:92:23
// |
// |-> request to server failed: "https://example.com/index.html", at examples/src/recovery.rs:99:23
9 changes: 9 additions & 0 deletions exn/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ pub trait ErrorExt: Error + Send + Sync + 'static {
{
Exn::new(self)
}

/// Raise this error as a new exception with recovery data.
#[track_caller]
fn with_recovery<R>(self, recovery: R) -> Exn<Self, R>
where
Self: Sized,
{
Exn::with_recovery(self, recovery)
}
}

impl<T> ErrorExt for T where T: Error + Send + Sync + 'static {}
66 changes: 65 additions & 1 deletion exn/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ use core::ops::Deref;
use core::panic::Location;

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

Expand Down Expand Up @@ -91,6 +92,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> {

Self {
frame: Box::new(frame),
recovery: (),
phantom: PhantomData,
}
}
Expand Down Expand Up @@ -119,12 +121,74 @@ impl<E: Error + Send + Sync + 'static> Exn<E> {
new_exn
}

/// Raise a new exception with recovery data; this will make the current exception a child of
/// the new one.
#[track_caller]
pub fn raise_with_recovery<T: Error + Send + Sync + 'static, R_>(
self,
err: T,
recovery: R_,
) -> Exn<T, R_> {
let mut new_exn = Exn::with_recovery(err, recovery);
new_exn.frame.children.push(*self.frame);
new_exn
}

/// Create a new exception with the given error, its children and recovery data.
#[track_caller]
pub fn raise_all_with_recovery<T, I, R>(error: E, children: I, recovery: R) -> Exn<E, R>
where
T: Error + Send + Sync + 'static,
I: IntoIterator,
I::Item: Into<Exn<T>>,
{
let mut new_exn = Exn::with_recovery(error, recovery);
for exn in children {
let exn = exn.into();
new_exn.frame.children.push(*exn.frame);
}
new_exn
}

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

impl<E: Error + Send + Sync + 'static, R> Exn<E, R> {
/// Create a new exception with the given error and a recovery value for the caller.
///
/// See [`Exn::new`] for more information.
#[track_caller]
pub fn with_recovery(error: E, recovery: R) -> Self {
Self {
frame: Exn::new(error).frame,
recovery,
phantom: PhantomData,
}
}

/// Discard the recovery value.
pub fn discard_recovery(self) -> Exn<E> {
Exn {
frame: self.frame,
recovery: (),
phantom: PhantomData,
}
}

/// Extract the recovery value.
pub fn recover(self) -> (R, Exn<E>) {
let err = Exn {
frame: self.frame,
recovery: (),
phantom: PhantomData,
};
(self.recovery, err)
}
}

impl<E> Deref for Exn<E>
where
E: Error + Send + Sync + 'static,
Expand Down
47 changes: 45 additions & 2 deletions exn/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ pub trait ResultExt {
where
A: Error + Send + Sync + 'static,
F: FnOnce() -> A;

/// Raise a new exception on the [`Exn`] inside the [`Result`] with additional data for
/// recovery.
///
/// Apply [`Exn::raise_with_recovery`] on the `Err` variant, refer to it for more information.
fn or_raise_with_recovery<A, F, R>(
self,
err: F,
recovery: R,
) -> core::result::Result<Self::Success, Exn<A, R>>
where
A: Error + Send + Sync + 'static,
F: FnOnce() -> A;
}

impl<T, E> ResultExt for core::result::Result<T, E>
Expand All @@ -54,9 +67,24 @@ where
Err(e) => Err(Exn::new(e).raise(err())),
}
}

fn or_raise_with_recovery<A, F, R>(
self,
err: F,
recovery: R,
) -> core::result::Result<Self::Success, Exn<A, R>>
where
A: Error + Send + Sync + 'static,
F: FnOnce() -> A,
{
match self {
Ok(v) => Ok(v),
Err(e) => Err(Exn::new(e).raise_with_recovery(err(), recovery)),
}
}
}

impl<T, E> ResultExt for core::result::Result<T, Exn<E>>
impl<T, E, R> ResultExt for core::result::Result<T, Exn<E, R>>
where
E: Error + Send + Sync + 'static,
{
Expand All @@ -71,7 +99,22 @@ where
{
match self {
Ok(v) => Ok(v),
Err(e) => Err(e.raise(err())),
Err(e) => Err(e.discard_recovery().raise(err())),
}
}

fn or_raise_with_recovery<A, F, R_>(
self,
err: F,
recovery: R_,
) -> core::result::Result<Self::Success, Exn<A, R_>>
where
A: Error + Send + Sync + 'static,
F: FnOnce() -> A,
{
match self {
Ok(v) => Ok(v),
Err(e) => Err(e.discard_recovery().raise_with_recovery(err(), recovery)),
}
}
}