use crate::Metadata;
macro_rules! classification_predicates {
() => {
pub fn dominant_class(&self) -> Option<Class> {
self.classify().dominant_class()
}
pub fn is_retryable(&self) -> bool {
self.classify().is_retryable()
}
pub fn is_resource_exhausted(&self) -> bool {
self.classify().is_resource_exhausted()
}
pub fn can_retry(&self) -> bool {
self.classify().can_retry()
}
pub fn can_retry_lenient(&self) -> bool {
self.classify().can_retry_lenient()
}
pub fn is_corrupted(&self) -> bool {
self.classify().is_corrupted()
}
pub fn is_not_found(&self) -> bool {
self.classify().is_not_found()
}
pub fn is_cancelled(&self) -> bool {
self.classify().is_cancelled()
}
pub fn is_permission_denied(&self) -> bool {
self.classify().is_permission_denied()
}
pub fn is_unauthenticated(&self) -> bool {
self.classify().is_unauthenticated()
}
pub fn is_conflict(&self) -> bool {
self.classify().is_conflict()
}
pub fn is_unsupported(&self) -> bool {
self.classify().is_unsupported()
}
pub fn is_validation(&self) -> bool {
self.classify().is_validation()
}
};
}
#[derive(Clone, Copy, Debug)]
pub struct DisplaySource<'a> {
error: &'a (dyn std::error::Error + 'static),
location: Option<&'static std::panic::Location<'static>>,
}
impl<'a> DisplaySource<'a> {
pub fn error(&self) -> &'a (dyn std::error::Error + 'static) {
self.error
}
pub fn location(&self) -> Option<&'static std::panic::Location<'static>> {
self.location
}
}
impl std::fmt::Display for DisplaySource<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self.error, f)?;
if !f.alternate()
&& let Some(location) = self.location
{
crate::write_location(f, location)?;
}
Ok(())
}
}
impl crate::Error {
pub fn into_exn(self) -> crate::Exn {
#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
{
crate::Exn::from_boxed_frame(self.into_frame())
}
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
{
crate::Exn::from_chain(self.inner)
}
}
pub fn iter_errors(&self) -> impl Iterator<Item = &(dyn std::error::Error + 'static)> + '_ {
self.iter_errors_with_locations().map(|source| source.error)
}
pub fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ {
Errors::new(self.iter_root())
.map(Node::display)
.filter(|source| !is_transparent_marker(source.error))
}
pub fn downcast_any_ref<T: std::error::Error + 'static>(&self) -> Option<&T> {
self.iter_errors().find_map(|error| error.downcast_ref())
}
pub fn probable_cause(&self) -> &(dyn std::error::Error + 'static) {
self.iter_root().probable_cause().unwrap_or_else(|| self.error())
}
pub fn metadata(&self) -> impl Iterator<Item = &Metadata> + '_ {
self.iter_errors()
.filter_map(|error| error.downcast_ref::<crate::Message>())
.map(|error| &error.values)
.filter(|values| !values.is_empty())
}
pub fn metadata_merged(&self) -> Metadata {
let mut merged = Metadata::new();
for values in self.metadata() {
merged.extend(values.iter().map(|(key, value)| (key.clone(), value.clone())));
}
merged
}
pub fn classify(&self) -> Classifications<'_> {
classify(self)
}
classification_predicates!();
}
impl<E: std::error::Error + Send + Sync + 'static> crate::Exn<E> {
pub fn classify(&self) -> Classifications<'_> {
Classifications(Errors::new(Node::Frame(self.frame().source_frame())))
}
classification_predicates!();
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Class {
Cancelled,
Corruption,
ResourceExhaustion(crate::ResourceExhaustionKind),
Validation,
Unsupported,
Unauthenticated,
PermissionDenied,
Conflict,
NotFound,
Retryable,
}
#[derive(Clone, Copy, Debug)]
pub struct Classification<'a> {
class: Class,
error: &'a (dyn std::error::Error + 'static),
}
pub fn classify<'a>(err: &'a (dyn std::error::Error + 'static)) -> Classifications<'a> {
Classifications(Errors::new(Node::boundary(err).unwrap_or(Node::Source {
error: err,
location: None,
source_owner: None,
})))
}
pub struct Classifications<'a>(Errors<'a>);
impl<'a> Iterator for Classifications<'a> {
type Item = Classification<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.0.find_map(classify_one)
}
}
impl Classifications<'_> {
pub fn dominant_class(self) -> Option<Class> {
self.map(|classification| classification.class()).min()
}
pub fn is_retryable(self) -> bool {
self.has(Class::Retryable)
}
pub fn can_retry(mut self) -> bool {
self.retry_policy(|node| node_can_retry(node).0)
}
pub fn can_retry_lenient(mut self) -> bool {
self.retry_policy(node_can_retry_lenient)
}
pub fn is_not_found(self) -> bool {
self.has(Class::NotFound)
}
pub fn is_cancelled(self) -> bool {
self.has(Class::Cancelled)
}
pub fn is_permission_denied(self) -> bool {
self.has(Class::PermissionDenied)
}
pub fn is_unauthenticated(self) -> bool {
self.has(Class::Unauthenticated)
}
pub fn is_conflict(self) -> bool {
self.has(Class::Conflict)
}
pub fn is_unsupported(self) -> bool {
self.has(Class::Unsupported)
}
pub fn is_validation(self) -> bool {
self.has(Class::Validation)
}
pub fn is_corrupted(self) -> bool {
self.has(Class::Corruption)
}
pub fn is_resource_exhausted(mut self) -> bool {
self.any(|classification| matches!(classification.class(), Class::ResourceExhaustion(_)))
}
fn retry_policy(&mut self, policy: impl Fn(Node<'_>) -> bool) -> bool {
let mut retryable = false;
for node in self.0.by_ref() {
if classify_one(node).is_some_and(|classification| classification.class() == Class::Cancelled) {
return false;
}
retryable |= policy(node);
}
retryable
}
pub fn has(mut self, class: Class) -> bool {
self.any(|classification| classification.class() == class)
}
}
impl<'a> Classification<'a> {
pub fn class(&self) -> Class {
self.class
}
pub fn error(&self) -> &'a (dyn std::error::Error + 'static) {
self.error
}
pub fn io_kind(&self) -> Option<std::io::ErrorKind> {
self.error.downcast_ref::<std::io::Error>().map(std::io::Error::kind)
}
}
fn classify_one(node: Node<'_>) -> Option<Classification<'_>> {
let mut error = node.display().error;
let class = if let Some(marker) = error.downcast_ref::<crate::ClassificationMarker>() {
let source_owner = match node {
Node::Frame(_) => None,
Node::Source { source_owner, .. } => source_owner,
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
Node::Chain { source_owner, .. } => source_owner,
};
error = std::error::Error::source(marker).or(source_owner).unwrap_or(error);
marker.class()
} else if let Some(error) = error.downcast_ref::<crate::Message>() {
error.class?
} else if error.is::<std::collections::TryReserveError>() {
Class::ResourceExhaustion(crate::ResourceExhaustionKind::AllocationFailure)
} else {
let error = error.downcast_ref::<std::io::Error>()?;
match error.kind() {
std::io::ErrorKind::NotFound => Class::NotFound,
std::io::ErrorKind::PermissionDenied => {
if error
.get_ref()
.is_some_and(|payload| has_explicit_authentication_challenge(payload))
{
return None;
}
Class::PermissionDenied
}
std::io::ErrorKind::Unsupported => Class::Unsupported,
std::io::ErrorKind::OutOfMemory => {
Class::ResourceExhaustion(crate::ResourceExhaustionKind::AllocationFailure)
}
_ => return None,
}
};
Some(Classification { class, error })
}
fn has_explicit_authentication_challenge(error: &(dyn std::error::Error + 'static)) -> bool {
classify(error).0.any(|node| {
let error = node.display().error;
error
.downcast_ref::<crate::Message>()
.is_some_and(|message| message.class == Some(Class::Unauthenticated))
|| error
.downcast_ref::<crate::ClassificationMarker>()
.is_some_and(|marker| marker.class() == Class::Unauthenticated)
})
}
fn node_can_retry(node: Node<'_>) -> (bool, Option<std::io::ErrorKind>) {
if classify_one(node).is_some_and(|classification| classification.class() == Class::Retryable) {
return (true, None);
}
let io_kind = node
.display()
.error
.downcast_ref::<std::io::Error>()
.map(std::io::Error::kind);
(
matches!(
io_kind,
Some(std::io::ErrorKind::Interrupted | std::io::ErrorKind::TimedOut)
),
io_kind,
)
}
fn node_can_retry_lenient(node: Node<'_>) -> bool {
let (can_retry, io_kind) = node_can_retry(node);
can_retry
|| io_kind.is_some_and(|kind| {
use std::io::ErrorKind::*;
matches!(
kind,
UnexpectedEof
| OutOfMemory
| BrokenPipe
| AddrInUse
| ConnectionAborted
| ConnectionReset
| ConnectionRefused
)
})
}
#[derive(Clone, Copy)]
enum Node<'a> {
Frame(&'a crate::exn::impls::FrameSource),
Source {
error: &'a (dyn std::error::Error + 'static),
location: Option<&'static std::panic::Location<'static>>,
source_owner: Option<&'a (dyn std::error::Error + 'static)>,
},
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
Chain {
node: &'a crate::types::ChainedError,
index: usize,
cursor: Option<usize>,
source_owner: Option<&'a (dyn std::error::Error + 'static)>,
},
}
impl<'a> Node<'a> {
fn boundary(error: &'a (dyn std::error::Error + 'static)) -> Option<Self> {
if let Some(error) = error.downcast_ref::<crate::Error>() {
Some(error.iter_root())
} else {
error.downcast_ref::<crate::exn::impls::FrameSource>().map(Node::Frame)
}
}
fn display(self) -> DisplaySource<'a> {
let (error, location) = match self {
Node::Frame(frame) => (
frame.error() as &(dyn std::error::Error + 'static),
Some(frame.location()),
),
Node::Source { error, location, .. } => (error, location),
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
Node::Chain { node, .. } => (node.err.error(), node.err.has_frame_location().then_some(node.location)),
};
DisplaySource { error, location }
}
fn children(self) -> std::collections::VecDeque<Node<'a>> {
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
let root = match self {
Node::Chain {
node,
index,
source_owner,
..
} => Node::Chain {
node,
index,
cursor: None,
source_owner,
},
root => root,
};
#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
let root = self;
let mut traversal = Errors::new(root);
traversal.children(root);
traversal.pending
}
fn probable_cause(self) -> Option<&'a (dyn std::error::Error + 'static)> {
let mut node = self;
let mut cause = None;
loop {
let mut pending = node.children();
let mut only_child = None;
while let Some(child) = pending.pop_front() {
if is_transparent_marker(child.display().error) {
pending.extend(child.children());
} else if only_child.replace(child).is_some() {
return cause;
}
}
node = match only_child {
Some(child) => child,
None => return cause,
};
cause = Some(node.display().error);
}
}
}
struct Errors<'a> {
root: Option<Node<'a>>,
previous: Option<Node<'a>>,
pending: std::collections::VecDeque<Node<'a>>,
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
chains: Vec<(usize, Option<&'a crate::types::ChainedError>)>,
}
impl<'a> Errors<'a> {
fn new(root: Node<'a>) -> Self {
Errors {
root: Some(root),
previous: None,
pending: Default::default(),
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
chains: Vec::new(),
}
}
fn source(
&mut self,
error: &'a (dyn std::error::Error + 'static),
location: Option<&'static std::panic::Location<'static>>,
) {
if let Some(node) = Node::boundary(error) {
self.pending.push_back(node);
} else if let Some(source) = native_source(error) {
self.pending
.push_back(source.downcast_ref::<crate::exn::impls::FrameSource>().map_or(
Node::Source {
error: source,
location: location.filter(|_| is_transparent_marker(error)),
source_owner: Some(error),
},
Node::Frame,
));
}
}
fn children(&mut self, node: Node<'a>) {
match node {
Node::Frame(frame) => {
self.source(frame.error(), Some(frame.location()));
self.pending
.extend(frame.children().iter().map(|frame| Node::Frame(frame.source_frame())));
}
Node::Source { error, location, .. } => self.source(error, location),
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
Node::Chain {
node, index, cursor, ..
} => {
if let Some(error) = node.err.error().downcast_ref::<crate::Error>() {
self.pending.push_back(error.iter_root());
}
let cursor = match cursor {
Some(cursor) => cursor,
None if node.source.is_none() => return,
None => {
self.chains.push((index + 1, node.source.as_deref()));
self.chains.len() - 1
}
};
let (child_index, next) = &mut self.chains[cursor];
while let Some(child) = next.filter(|child| child.logical_parent.is_some_and(|parent| parent < index)) {
*child_index += 1;
*next = child.source.as_deref();
}
while let Some(child) = next.filter(|child| child.logical_parent == Some(index)) {
self.pending.push_back(Node::Chain {
node: child,
index: *child_index,
cursor: Some(cursor),
source_owner: child.err.is_native_source().then(|| node.err.error()),
});
*child_index += 1;
*next = child.source.as_deref();
}
}
}
}
}
impl<'a> Iterator for Errors<'a> {
type Item = Node<'a>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(previous) = self.previous.take() {
self.children(previous);
}
let node = self.root.take().or_else(|| self.pending.pop_front())?;
self.previous = Some(node);
Some(node)
}
}
impl crate::exn::Frame {
pub(crate) fn probable_cause_inner(&self) -> Option<&(dyn std::error::Error + 'static)> {
Node::Frame(self.source_frame()).probable_cause()
}
pub(crate) fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ {
Errors::new(Node::Frame(self.source_frame()))
.map(Node::display)
.filter(|source| !is_transparent_marker(source.error))
}
}
#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
mod _impl {
use crate::{Error, Exn};
use std::fmt::Formatter;
impl Error {
#[expect(
clippy::unnecessary_box_returns,
reason = "erasure retains the existing frame allocation"
)]
pub(crate) fn into_frame(self) -> Box<crate::exn::Frame> {
let (Inner::Exn(frame) | Inner::ExnAsError(frame)) = self.inner;
frame
}
pub fn error(&self) -> &(dyn std::error::Error + 'static) {
self.inner.frame().error()
}
pub(super) fn iter_root(&self) -> super::Node<'_> {
super::Node::Frame(self.inner.frame().source_frame())
}
}
pub(crate) enum Inner {
ExnAsError(Box<crate::exn::Frame>),
Exn(Box<crate::exn::Frame>),
}
impl Inner {
pub(crate) fn frame(&self) -> &crate::exn::Frame {
match self {
Inner::ExnAsError(f) | Inner::Exn(f) => f,
}
}
}
impl Error {
#[track_caller]
pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
Error {
inner: Inner::ExnAsError(Exn::new(error).into()),
}
}
#[track_caller]
pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
Self::from_error(crate::exn::Untyped::from_boxed(error))
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.inner {
Inner::ExnAsError(err) => std::fmt::Display::fmt(err.error(), f),
Inner::Exn(frame) => std::fmt::Display::fmt(frame, f),
}
}
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.inner {
Inner::ExnAsError(err) => std::fmt::Debug::fmt(err.error(), f),
Inner::Exn(frame) => std::fmt::Debug::fmt(frame, f),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
std::error::Error::source(self.inner.frame().source_frame())
}
}
impl<E> From<Exn<E>> for Error
where
E: std::error::Error + Send + Sync + 'static,
{
fn from(err: Exn<E>) -> Self {
Error {
inner: Inner::Exn(err.into()),
}
}
}
}
#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
pub(super) use _impl::Inner;
#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
mod _impl {
use crate::{Error, Exn};
use std::fmt::Formatter;
impl Error {
pub fn error(&self) -> &(dyn std::error::Error + 'static) {
self.inner.err.error()
}
pub(super) fn iter_root(&self) -> super::Node<'_> {
super::Node::Chain {
node: &self.inner,
index: 0,
cursor: None,
source_owner: None,
}
}
}
impl Error {
#[track_caller]
pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
Error {
inner: Exn::new(error).into_chain(),
}
}
#[track_caller]
pub fn from_boxed(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
Self::from_error(crate::exn::Untyped::from_boxed(error))
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
return self.fmt_chain(f, true);
}
if super::is_transparent_marker(self.error())
&& let Some(diagnostic) = self.iter_errors_with_locations().next()
{
return std::fmt::Display::fmt(&diagnostic, f);
}
std::fmt::Display::fmt(&self.inner, f)
}
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.fmt_chain(f, false)
}
}
impl Error {
pub(crate) fn fmt_chain(&self, f: &mut Formatter<'_>, inline: bool) -> std::fmt::Result {
let write_error = |error: super::DisplaySource<'_>, f: &mut Formatter<'_>| -> std::fmt::Result {
crate::exn::impls::ErrorMode::Display.fmt(error.error(), f)?;
if !inline
&& !f.alternate()
&& let Some(location) = error.location()
{
crate::write_location(f, location)?;
}
Ok(())
};
let mut errors = self
.iter_errors_with_locations()
.filter(|source| !source.error().is::<Error>());
let Some(error) = errors.next() else {
return std::fmt::Display::fmt(&self.inner, f);
};
write_error(error, f)?;
for (index, error) in errors.enumerate() {
if inline {
write!(f, ": ")?;
} else {
if index == 0 {
write!(f, "\n\nCaused by:")?;
}
write!(f, "\n {index}: ")?;
}
write_error(error, f)?;
}
Ok(())
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.inner.source()
}
}
impl<E> From<Exn<E>> for Error
where
E: std::error::Error + Send + Sync + 'static,
{
fn from(err: Exn<E>) -> Self {
Error {
inner: err.into_chain(),
}
}
}
}
impl From<crate::Message> for crate::Error {
#[track_caller]
fn from(err: crate::Message) -> Self {
crate::Exn::new(err).into()
}
}
pub(crate) fn native_source<'a>(
err: &'a (dyn std::error::Error + 'static),
) -> Option<&'a (dyn std::error::Error + 'static)> {
match err.downcast_ref::<std::io::Error>() {
Some(err) => err.get_ref().map(|err| err as _),
None => err.source(),
}
}
pub(crate) fn is_transparent_marker(mut error: &(dyn std::error::Error + 'static)) -> bool {
while let Some(nested) = error.downcast_ref::<crate::Error>() {
error = nested.error();
}
error.is::<crate::ClassificationMarker>()
}