Skip to main content

handle_this/
handled.rs

1//! Core error type and related structures.
2
3#[cfg(not(feature = "std"))]
4use alloc::{
5    borrow::Cow,
6    boxed::Box,
7    format,
8    string::{String, ToString},
9    vec,
10    vec::Vec,
11};
12
13#[cfg(feature = "std")]
14use std::borrow::Cow;
15#[cfg(feature = "std")]
16use std::sync::OnceLock;
17
18use core::fmt;
19
20#[cfg(feature = "std")]
21use std::error::Error as StdError;
22
23// ============================================================
24// Core types
25// ============================================================
26
27/// Error wrapper that captures context and stack traces for any error.
28///
29/// `Handled<E>` wraps an error type `E` while adding:
30/// - A trace of frames showing where the error propagated
31/// - Context messages and key-value attachments at each frame
32///
33/// # Type Parameters
34///
35/// - `E` - The underlying error type. Defaults to `Error` for type-erased
36///   errors (the common case with `handle!` macro).
37///
38/// The `handle!` macro produces `Handled<Error>` (aliased as `Handled`),
39/// with the concrete error type preserved inside for downcasting via `TryCatch`.
40///
41/// # Examples
42///
43/// ```
44/// use handle_this::{handle, Handled, Result};
45///
46/// fn read_file(path: &str) -> Result<String> {
47///     handle! { try { std::fs::read_to_string(path)? } with "reading config" }
48/// }
49/// ```
50#[derive(Debug)]
51pub struct Handled<E = Error> {
52    pub(crate) source: E,
53    /// Lazy message - only computed when accessed via `message()`.
54    /// This avoids expensive `to_string()` calls on every error creation.
55    pub(crate) message: OnceLock<String>,
56    /// Location trace - inline storage for ≤8 frames (common case), heap for overflow.
57    /// Avoids allocation for typical error traces.
58    pub(crate) locations: LocationVec,
59    /// Context entries - expensive, only allocated when .ctx()/.kv() used.
60    /// Each entry references a location by index.
61    pub(crate) contexts: Option<Vec<ContextEntry>>,
62    /// Previous error in chain - used by `chain_after` for `catch any/all`.
63    /// Stored separately to preserve the root error's type for `catch Type`.
64    #[cfg(feature = "std")]
65    pub(crate) chained: Option<Box<Handled<Error>>>,
66}
67
68/// Type-erased error wrapper for when you don't need to preserve the concrete type.
69///
70/// This is a newtype wrapper around `Box<dyn StdError>` that enables proper trait
71/// resolution for typed catches. It does NOT implement `Error` itself (to avoid
72/// trait impl conflicts), but provides access to the inner error.
73#[cfg(feature = "std")]
74#[derive(Debug)]
75pub struct Error(Box<dyn StdError + Send + Sync + 'static>);
76
77#[cfg(not(feature = "std"))]
78#[derive(Debug)]
79pub struct Error(Box<dyn fmt::Debug + fmt::Display + Send + Sync + 'static>);
80
81#[cfg(feature = "std")]
82impl Error {
83    /// Create from any error type.
84    #[inline]
85    pub fn new<E: StdError + Send + Sync + 'static>(e: E) -> Self {
86        Self(Box::new(e))
87    }
88
89    /// Create from a boxed error.
90    #[inline]
91    pub fn from_box(e: Box<dyn StdError + Send + Sync + 'static>) -> Self {
92        Self(e)
93    }
94
95    /// Get the inner error as a trait object reference.
96    #[inline]
97    pub fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static) {
98        self.0.as_ref()
99    }
100
101    /// Get the inner error as a trait object reference (non-Send/Sync for Error trait compat).
102    #[inline]
103    pub fn as_dyn_error(&self) -> &(dyn StdError + 'static) {
104        self.0.as_ref()
105    }
106
107    /// Try to downcast to a specific error type.
108    #[inline]
109    pub fn downcast_ref<T: StdError + 'static>(&self) -> Option<&T> {
110        self.0.downcast_ref::<T>()
111    }
112
113    /// Try to downcast and consume the error.
114    #[inline]
115    pub fn downcast<T: StdError + 'static>(self) -> core::result::Result<T, Self> {
116        match self.0.downcast::<T>() {
117            Ok(e) => Ok(*e),
118            Err(e) => Err(Self(e)),
119        }
120    }
121
122    /// Get the inner boxed error.
123    pub fn into_inner(self) -> Box<dyn StdError + Send + Sync + 'static> {
124        self.0
125    }
126}
127
128#[cfg(not(feature = "std"))]
129impl Error {
130    /// Create from any error type.
131    #[inline]
132    pub fn new<E: fmt::Debug + fmt::Display + Send + Sync + 'static>(e: E) -> Self {
133        Self(Box::new(e))
134    }
135
136    /// Create from a boxed error.
137    #[inline]
138    pub fn from_box(e: Box<dyn fmt::Debug + fmt::Display + Send + Sync + 'static>) -> Self {
139        Self(e)
140    }
141}
142
143impl fmt::Display for Error {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        self.0.fmt(f)
146    }
147}
148
149// From impl for Error - enables ? operator in try blocks
150// This doesn't conflict with From<T> for T because Error doesn't implement Error
151#[cfg(feature = "std")]
152impl<E: StdError + Send + Sync + 'static> From<E> for Error {
153    fn from(e: E) -> Self {
154        Error::new(e)
155    }
156}
157
158/// Location in source code - cheap, no allocation.
159#[derive(Debug, Clone, Copy)]
160pub(crate) struct Location {
161    pub(crate) file: &'static str,  // Always from file!() macro
162    pub(crate) line: u32,
163    pub(crate) col: u32,
164}
165
166/// Inline storage for locations - avoids heap allocation for common case.
167/// Stores up to 4 frames inline (covers most error traces); overflows to Vec for deeper traces.
168const INLINE_CAPACITY: usize = 4;
169
170#[derive(Debug)]
171pub(crate) struct LocationVec {
172    len: u8,
173    inline: [core::mem::MaybeUninit<Location>; INLINE_CAPACITY],
174    overflow: Option<Vec<Location>>,
175}
176
177impl Clone for LocationVec {
178    fn clone(&self) -> Self {
179        let mut new = Self::new();
180        for loc in self.iter() {
181            new.push(*loc);
182        }
183        new
184    }
185}
186
187impl LocationVec {
188    #[inline]
189    pub const fn new() -> Self {
190        Self {
191            len: 0,
192            inline: [core::mem::MaybeUninit::uninit(); INLINE_CAPACITY],
193            overflow: None,
194        }
195    }
196
197    #[inline]
198    pub fn push(&mut self, loc: Location) {
199        let idx = self.len as usize;
200        if idx < INLINE_CAPACITY {
201            self.inline[idx] = core::mem::MaybeUninit::new(loc);
202            self.len += 1;
203        } else if idx < DEFAULT_LOCATION_LIMIT {
204            // Spill to overflow
205            let overflow = self.overflow.get_or_insert_with(Vec::new);
206            overflow.push(loc);
207            self.len += 1;
208        }
209        // Silently drop if at limit
210    }
211
212    #[inline]
213    pub fn len(&self) -> usize {
214        self.len as usize
215    }
216
217    #[inline]
218    pub fn is_empty(&self) -> bool {
219        self.len == 0
220    }
221
222    #[inline]
223    pub fn iter(&self) -> impl Iterator<Item = &Location> + '_ {
224        let inline_count = core::cmp::min(self.len as usize, INLINE_CAPACITY);
225        let inline_iter = (0..inline_count).map(move |i| {
226            // SAFETY: We only read initialized elements (i < inline_count)
227            unsafe { self.inline[i].assume_init_ref() }
228        });
229        let overflow_iter = self.overflow.iter().flat_map(|v| v.iter());
230        inline_iter.chain(overflow_iter)
231    }
232}
233
234/// Context entry attached to a specific location - expensive, has allocations.
235#[derive(Debug, Clone)]
236pub(crate) struct ContextEntry {
237    pub(crate) location_idx: u16,  // Which location this attaches to
238    pub(crate) message: Option<String>,
239    pub(crate) attachments: Vec<(Cow<'static, str>, Value)>,
240}
241
242/// A typed value for structured logging attachments.
243///
244/// Preserves type information for JSON serialization and log aggregation systems.
245#[derive(Debug, Clone, PartialEq)]
246pub enum Value {
247    /// String value
248    String(String),
249    /// Signed integer (i8, i16, i32, i64, isize)
250    Int(i64),
251    /// Unsigned integer (u8, u16, u32, u64, usize)
252    Uint(u64),
253    /// Floating point (f32, f64)
254    Float(f64),
255    /// Boolean
256    Bool(bool),
257    /// Null/None value
258    Null,
259}
260
261impl Value {
262    /// Create a Value from any supported type.
263    pub fn from<T: IntoValue>(v: T) -> Self {
264        v.into_value()
265    }
266}
267
268impl fmt::Display for Value {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        match self {
271            Value::String(s) => write!(f, "{}", s),
272            Value::Int(n) => write!(f, "{}", n),
273            Value::Uint(n) => write!(f, "{}", n),
274            Value::Float(n) => write!(f, "{}", n),
275            Value::Bool(b) => write!(f, "{}", b),
276            Value::Null => write!(f, "null"),
277        }
278    }
279}
280
281// Allow comparing Value with string types for convenience in tests
282impl PartialEq<str> for Value {
283    fn eq(&self, other: &str) -> bool {
284        match self {
285            Value::String(s) => s == other,
286            _ => false,
287        }
288    }
289}
290
291impl PartialEq<&str> for Value {
292    fn eq(&self, other: &&str) -> bool {
293        self == *other
294    }
295}
296
297impl PartialEq<String> for Value {
298    fn eq(&self, other: &String) -> bool {
299        self == other.as_str()
300    }
301}
302
303impl PartialEq<i64> for Value {
304    fn eq(&self, other: &i64) -> bool {
305        match self {
306            Value::Int(n) => n == other,
307            _ => false,
308        }
309    }
310}
311
312impl PartialEq<u64> for Value {
313    fn eq(&self, other: &u64) -> bool {
314        match self {
315            Value::Uint(n) => n == other,
316            _ => false,
317        }
318    }
319}
320
321impl PartialEq<f64> for Value {
322    fn eq(&self, other: &f64) -> bool {
323        match self {
324            Value::Float(n) => (n - other).abs() < f64::EPSILON,
325            _ => false,
326        }
327    }
328}
329
330impl PartialEq<bool> for Value {
331    fn eq(&self, other: &bool) -> bool {
332        match self {
333            Value::Bool(b) => b == other,
334            _ => false,
335        }
336    }
337}
338
339/// Trait for converting types into Value.
340pub trait IntoValue {
341    fn into_value(self) -> Value;
342}
343
344impl IntoValue for Value {
345    fn into_value(self) -> Value {
346        self
347    }
348}
349
350impl IntoValue for String {
351    fn into_value(self) -> Value {
352        Value::String(self)
353    }
354}
355
356impl IntoValue for &str {
357    fn into_value(self) -> Value {
358        Value::String(self.to_string())
359    }
360}
361
362impl<'a> IntoValue for Cow<'a, str> {
363    fn into_value(self) -> Value {
364        Value::String(self.into_owned())
365    }
366}
367
368impl IntoValue for bool {
369    fn into_value(self) -> Value {
370        Value::Bool(self)
371    }
372}
373
374impl IntoValue for i8 {
375    fn into_value(self) -> Value {
376        Value::Int(self as i64)
377    }
378}
379
380impl IntoValue for i16 {
381    fn into_value(self) -> Value {
382        Value::Int(self as i64)
383    }
384}
385
386impl IntoValue for i32 {
387    fn into_value(self) -> Value {
388        Value::Int(self as i64)
389    }
390}
391
392impl IntoValue for i64 {
393    fn into_value(self) -> Value {
394        Value::Int(self)
395    }
396}
397
398impl IntoValue for isize {
399    fn into_value(self) -> Value {
400        Value::Int(self as i64)
401    }
402}
403
404impl IntoValue for u8 {
405    fn into_value(self) -> Value {
406        Value::Uint(self as u64)
407    }
408}
409
410impl IntoValue for u16 {
411    fn into_value(self) -> Value {
412        Value::Uint(self as u64)
413    }
414}
415
416impl IntoValue for u32 {
417    fn into_value(self) -> Value {
418        Value::Uint(self as u64)
419    }
420}
421
422impl IntoValue for u64 {
423    fn into_value(self) -> Value {
424        Value::Uint(self)
425    }
426}
427
428impl IntoValue for usize {
429    fn into_value(self) -> Value {
430        Value::Uint(self as u64)
431    }
432}
433
434impl IntoValue for f32 {
435    fn into_value(self) -> Value {
436        Value::Float(self as f64)
437    }
438}
439
440impl IntoValue for f64 {
441    fn into_value(self) -> Value {
442        Value::Float(self)
443    }
444}
445
446impl<T: IntoValue> IntoValue for Option<T> {
447    fn into_value(self) -> Value {
448        match self {
449            Some(v) => v.into_value(),
450            None => Value::Null,
451        }
452    }
453}
454
455// Reference implementation - deref and convert
456impl<T: IntoValue + Clone> IntoValue for &T {
457    fn into_value(self) -> Value {
458        self.clone().into_value()
459    }
460}
461
462/// Default limits for trace depth
463pub const DEFAULT_LOCATION_LIMIT: usize = 32;
464pub const DEFAULT_CONTEXT_LIMIT: usize = 8;
465
466/// View into a single frame of the error trace.
467#[derive(Debug, Clone)]
468pub struct FrameView<'a> {
469    /// Source file path
470    pub file: &'a str,
471    /// Line number
472    pub line: u32,
473    /// Column number
474    pub col: u32,
475    /// Optional context message
476    pub context: Option<&'a str>,
477    /// Key-value attachments (internal)
478    attachments_inner: &'a [(Cow<'static, str>, Value)],
479}
480
481impl<'a> FrameView<'a> {
482    /// Iterate over key-value attachments on this frame with typed values.
483    pub fn attachments(&self) -> impl Iterator<Item = (&'a str, &'a Value)> {
484        self.attachments_inner.iter().map(|(k, v)| (k.as_ref(), v))
485    }
486
487    /// Iterate over key-value attachments as strings (for backwards compatibility).
488    pub fn attachments_str(&self) -> impl Iterator<Item = (&'a str, String)> + 'a {
489        self.attachments_inner.iter().map(|(k, v)| (k.as_ref(), v.to_string()))
490    }
491}
492
493// ============================================================
494// TryCatch trait - enables typed catches with both concrete and erased errors
495// ============================================================
496
497/// Trait for attempting to extract a specific error type from a Handled wrapper.
498///
499/// This enables typed `catch Type(e) { }` patterns to work with both:
500/// - `Handled<E>` where `E` is the exact type (direct access)
501/// - `Handled<Error>` where we need runtime downcasting
502#[doc(hidden)]
503pub trait TryCatch<Target> {
504    fn try_catch(&self) -> Option<&Target>;
505}
506
507// Direct match - when the source IS the target type
508impl<T> TryCatch<T> for T {
509    #[inline]
510    fn try_catch(&self) -> Option<&T> {
511        Some(self)
512    }
513}
514
515// Downcast match - for type-erased errors (newtype, doesn't impl Error)
516#[cfg(feature = "std")]
517impl<T: StdError + 'static> TryCatch<T> for Error {
518    #[inline]
519    fn try_catch(&self) -> Option<&T> {
520        self.downcast_ref::<T>()
521    }
522}
523
524// ============================================================
525// StringError helper
526// ============================================================
527
528#[derive(Debug)]
529pub struct StringError(pub(crate) String);
530
531impl fmt::Display for StringError {
532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533        f.write_str(&self.0)
534    }
535}
536
537#[cfg(feature = "std")]
538impl StdError for StringError {}
539
540// ============================================================
541// Handled<E> implementation - generic over error type
542// ============================================================
543
544impl<E> Handled<E> {
545    /// Create a new Handled wrapper around an error.
546    /// Message is computed lazily on first access.
547    #[cfg(feature = "std")]
548    #[inline]
549    pub fn new(source: E) -> Self
550    where
551        E: fmt::Display,
552    {
553        Self {
554            source,
555            message: OnceLock::new(),
556            locations: LocationVec::new(),
557            contexts: None,
558            chained: None,
559        }
560    }
561
562    #[cfg(not(feature = "std"))]
563    #[inline]
564    pub fn new(source: E) -> Self
565    where
566        E: fmt::Display,
567    {
568        Self {
569            source,
570            message: OnceLock::new(),
571            locations: LocationVec::new(),
572            contexts: None,
573        }
574    }
575
576    /// Add a frame with location information.
577    /// This is cheap - just stores file:line:col, no allocation beyond Vec growth.
578    #[doc(hidden)]
579    #[inline]
580    pub fn frame(mut self, file: &'static str, line: u32, col: u32) -> Self {
581        if self.locations.len() < DEFAULT_LOCATION_LIMIT {
582            self.locations.push(Location { file, line, col });
583        }
584        self
585    }
586
587    /// Add context message to the most recent frame.
588    /// This is expensive - allocates the contexts Vec if needed.
589    #[doc(hidden)]
590    #[inline]
591    pub fn ctx(mut self, msg: impl Into<String>) -> Self {
592        let location_idx = self.locations.len().saturating_sub(1) as u16;
593        let contexts = self.contexts.get_or_insert_with(Vec::new);
594
595        if contexts.len() < DEFAULT_CONTEXT_LIMIT {
596            // Check if we already have a context for this location
597            if let Some(entry) = contexts.iter_mut().find(|e| e.location_idx == location_idx) {
598                entry.message = Some(msg.into());
599            } else {
600                contexts.push(ContextEntry {
601                    location_idx,
602                    message: Some(msg.into()),
603                    attachments: Vec::new(),
604                });
605            }
606        }
607        self
608    }
609
610    /// Add a scope frame - creates a new frame for hierarchical context.
611    /// Unlike `.ctx()` which modifies the current frame, this adds a new frame.
612    #[doc(hidden)]
613    #[inline]
614    pub fn scope(
615        mut self,
616        file: &'static str,
617        line: u32,
618        col: u32,
619        msg: impl Into<String>,
620    ) -> Self {
621        if self.locations.len() < DEFAULT_LOCATION_LIMIT {
622            self.locations.push(Location { file, line, col });
623            let location_idx = (self.locations.len() - 1) as u16;
624
625            let contexts = self.contexts.get_or_insert_with(Vec::new);
626            if contexts.len() < DEFAULT_CONTEXT_LIMIT {
627                contexts.push(ContextEntry {
628                    location_idx,
629                    message: Some(msg.into()),
630                    attachments: Vec::new(),
631                });
632            }
633        }
634        self
635    }
636
637    /// Add a scope frame with key-value attachments.
638    #[doc(hidden)]
639    #[inline]
640    pub fn scope_kv(
641        mut self,
642        file: &'static str,
643        line: u32,
644        col: u32,
645        msg: impl Into<String>,
646        attachments: Vec<(Cow<'static, str>, Value)>,
647    ) -> Self {
648        if self.locations.len() < DEFAULT_LOCATION_LIMIT {
649            self.locations.push(Location { file, line, col });
650            let location_idx = (self.locations.len() - 1) as u16;
651
652            let contexts = self.contexts.get_or_insert_with(Vec::new);
653            if contexts.len() < DEFAULT_CONTEXT_LIMIT {
654                contexts.push(ContextEntry {
655                    location_idx,
656                    message: Some(msg.into()),
657                    attachments,
658                });
659            }
660        }
661        self
662    }
663
664    /// Add key-value attachment to the most recent frame with typed value.
665    #[doc(hidden)]
666    #[inline]
667    pub fn kv(mut self, key: &'static str, val: impl IntoValue) -> Self {
668        let location_idx = self.locations.len().saturating_sub(1) as u16;
669        let contexts = self.contexts.get_or_insert_with(Vec::new);
670
671        // Find or create context entry for this location
672        if let Some(entry) = contexts.iter_mut().find(|e| e.location_idx == location_idx) {
673            entry.attachments.push((Cow::Borrowed(key), val.into_value()));
674        } else if contexts.len() < DEFAULT_CONTEXT_LIMIT {
675            contexts.push(ContextEntry {
676                location_idx,
677                message: None,
678                attachments: vec![(Cow::Borrowed(key), val.into_value())],
679            });
680        }
681        self
682    }
683
684    /// Get the error message, computing it lazily on first access.
685    pub fn message(&self) -> &str
686    where
687        E: fmt::Display,
688    {
689        self.message.get_or_init(|| self.source.to_string())
690    }
691
692    /// Try to get a reference to a specific error type.
693    ///
694    /// This works with both concrete error types (direct access) and
695    /// type-erased errors (runtime downcasting).
696    #[doc(hidden)]
697    #[inline]
698    pub fn try_catch<Target>(&self) -> Option<&Target>
699    where
700        E: TryCatch<Target>,
701    {
702        self.source.try_catch()
703    }
704
705    /// Get the underlying error source.
706    pub fn source_ref(&self) -> &E {
707        &self.source
708    }
709
710    /// Consume and return the underlying error.
711    pub fn into_source(self) -> E {
712        self.source
713    }
714
715    /// Iterate over frames in the trace.
716    /// Combines locations with their optional contexts.
717    pub fn frames(&self) -> impl Iterator<Item = FrameView<'_>> {
718        let contexts = self.contexts.as_ref();
719        self.locations.iter().enumerate().map(move |(idx, loc)| {
720            let idx = idx as u16;
721            let ctx = contexts.and_then(|c| c.iter().find(|e| e.location_idx == idx));
722            FrameView {
723                file: loc.file,
724                line: loc.line,
725                col: loc.col,
726                context: ctx.and_then(|c| c.message.as_deref()),
727                attachments_inner: ctx.map(|c| c.attachments.as_slice()).unwrap_or(&[]),
728            }
729        })
730    }
731
732    /// Number of location frames in the trace.
733    pub fn depth(&self) -> usize {
734        self.locations.len()
735    }
736
737    /// Whether the trace is empty.
738    pub fn is_empty(&self) -> bool {
739        self.locations.is_empty()
740    }
741
742    /// Number of context entries (frames with messages/attachments).
743    pub fn context_count(&self) -> usize {
744        self.contexts.as_ref().map(|c| c.len()).unwrap_or(0)
745    }
746
747    /// Add a frame at the caller's location.
748    #[track_caller]
749    pub fn here(self) -> Self {
750        let loc = core::panic::Location::caller();
751        self.frame(loc.file(), loc.line(), loc.column())
752    }
753
754    /// Convert to a type-erased Handled.
755    #[cfg(feature = "std")]
756    pub fn erase(self) -> Handled<Error>
757    where
758        E: StdError + Send + Sync + 'static,
759    {
760        Handled {
761            source: Error::new(self.source),
762            message: self.message,
763            locations: self.locations,
764            contexts: self.contexts,
765            chained: self.chained,
766        }
767    }
768
769    /// Map the error type while preserving context.
770    pub fn map_err<F, O>(self, f: F) -> Handled<O>
771    where
772        F: FnOnce(E) -> O,
773        O: fmt::Display,
774    {
775        let new_source = f(self.source);
776        Handled {
777            source: new_source,
778            message: OnceLock::new(),  // Lazy - will compute from new source
779            locations: self.locations,
780            contexts: self.contexts,
781            #[cfg(feature = "std")]
782            chained: self.chained,
783        }
784    }
785}
786
787// ============================================================
788// Handled<Error> specific methods (type-erased)
789// ============================================================
790
791impl Handled<Error> {
792    /// Wrap any error, avoiding double-boxing if already Handled.
793    /// Message is computed lazily on first access.
794    #[cfg(feature = "std")]
795    #[inline]
796    pub fn wrap<E>(e: E) -> Self
797    where
798        E: StdError + Send + Sync + 'static,
799    {
800        use core::any::TypeId;
801        if TypeId::of::<E>() == TypeId::of::<Self>() {
802            // SAFETY: TypeId guarantees E is Handled. We read it out and forget
803            // the original to avoid double-drop.
804            unsafe {
805                let handled = core::ptr::read(&e as *const E as *const Self);
806                core::mem::forget(e);
807                handled
808            }
809        } else {
810            Self {
811                source: Error::new(e),
812                message: OnceLock::new(),
813                locations: LocationVec::new(),
814                contexts: None,
815                chained: None,
816            }
817        }
818    }
819
820    /// Wrap a boxed error into a type-erased Handled.
821    /// If the boxed error is already a Handled, unwrap it to avoid double-wrapping.
822    /// Message is computed lazily on first access.
823    #[cfg(feature = "std")]
824    #[inline]
825    pub fn wrap_box(e: Box<dyn StdError + Send + Sync + 'static>) -> Self {
826        // Check if it's already a Handled<Error>
827        match e.downcast::<Self>() {
828            Ok(handled) => *handled,
829            Err(e) => {
830                Self {
831                    source: Error::from_box(e),
832                    message: OnceLock::new(),
833                    locations: LocationVec::new(),
834                    contexts: None,
835                    chained: None,
836                }
837            }
838        }
839    }
840
841    /// Wrap an Error directly.
842    /// Message is computed lazily on first access.
843    #[cfg(feature = "std")]
844    #[inline]
845    pub fn wrap_erased(e: Error) -> Self {
846        Self {
847            source: e,
848            message: OnceLock::new(),
849            locations: LocationVec::new(),
850            contexts: None,
851            chained: None,
852        }
853    }
854
855    /// Wrap a boxed error and add a frame in one operation.
856    /// Equivalent to wrap_box().frame() but as a single call.
857    #[doc(hidden)]
858    #[cfg(feature = "std")]
859    #[inline]
860    pub fn wrap_box_with_frame(
861        e: Box<dyn StdError + Send + Sync + 'static>,
862        file: &'static str,
863        line: u32,
864        col: u32,
865    ) -> Self {
866        Self::wrap_box(e).frame(file, line, col)
867    }
868
869    #[cfg(not(feature = "std"))]
870    #[inline]
871    pub fn wrap<E>(e: E) -> Self
872    where
873        E: fmt::Debug + fmt::Display + Send + Sync + 'static,
874    {
875        use core::any::TypeId;
876        if TypeId::of::<E>() == TypeId::of::<Self>() {
877            unsafe {
878                let handled = core::ptr::read(&e as *const E as *const Self);
879                core::mem::forget(e);
880                handled
881            }
882        } else {
883            Self {
884                source: Error::new(e),
885                message: OnceLock::new(),
886                locations: LocationVec::new(),
887                contexts: None,
888            }
889        }
890    }
891
892    /// Create from a message string.
893    /// Message is pre-initialized since we already have it.
894    #[cfg(feature = "std")]
895    #[inline]
896    pub fn msg(message: impl Into<String>) -> Self {
897        let message = message.into();
898        let msg_lock = OnceLock::new();
899        let _ = msg_lock.set(message.clone());
900        Self {
901            source: Error::new(StringError(message)),
902            message: msg_lock,
903            locations: LocationVec::new(),
904            contexts: None,
905            chained: None,
906        }
907    }
908
909    #[cfg(not(feature = "std"))]
910    #[inline]
911    pub fn msg(message: impl Into<String>) -> Self {
912        let message = message.into();
913        let msg_lock = OnceLock::new();
914        let _ = msg_lock.set(message.clone());
915        Self {
916            source: Error::new(StringError(message)),
917            message: msg_lock,
918            locations: LocationVec::new(),
919            contexts: None,
920        }
921    }
922
923    /// Chain this error after a previous error.
924    ///
925    /// Used by `try any` to link all failed attempts together so that
926    /// `catch any`, `throw any`, and `inspect any` can find errors
927    /// from any iteration.
928    ///
929    /// The previous error becomes accessible via `chain_any`/`chain_all`.
930    /// The root error type is preserved for `catch Type` matching.
931    #[doc(hidden)]
932    #[cfg(feature = "std")]
933    pub fn chain_after(mut self, previous: Self) -> Self {
934        // Flatten any existing chain from self
935        let existing_chain = self.chained.take();
936
937        // Build chain: previous -> existing_chain (if any)
938        let new_previous = if let Some(existing) = existing_chain {
939            // previous's chain gets extended with existing
940            let mut prev = previous;
941            prev.chained = Some(existing);
942            prev
943        } else {
944            previous
945        };
946
947        self.chained = Some(Box::new(new_previous));
948        self
949    }
950
951    /// Get the root error as a trait object.
952    #[cfg(feature = "std")]
953    pub fn root(&self) -> &(dyn StdError + 'static) {
954        self.source.as_dyn_error()
955    }
956
957    /// Try to downcast to a specific error type.
958    #[cfg(feature = "std")]
959    #[inline]
960    pub fn downcast_ref<T: StdError + 'static>(&self) -> Option<&T> {
961        self.source.downcast_ref::<T>()
962    }
963
964    /// Try to downcast and consume the error.
965    #[cfg(feature = "std")]
966    #[inline]
967    pub fn downcast<T: StdError + 'static>(self) -> core::result::Result<T, Self> {
968        if self.source.downcast_ref::<T>().is_some() {
969            let Self {
970                source,
971                locations,
972                contexts,
973                message,
974                chained,
975            } = self;
976            match source.downcast::<T>() {
977                Ok(e) => Ok(e),
978                Err(source) => Err(Self {
979                    source,
980                    locations,
981                    contexts,
982                    message,
983                    chained,
984                }),
985            }
986        } else {
987            Err(self)
988        }
989    }
990
991    /// Find the first error of type `T` in the cause chain.
992    ///
993    /// Walks the error chain via `std::error::Error::source()` and returns
994    /// a reference to the first error that matches type `T`.
995    ///
996    /// # Example
997    ///
998    /// ```
999    /// use handle_this::{Handled, Result};
1000    /// use std::io;
1001    ///
1002    /// fn check_chain(err: &Handled) {
1003    ///     if let Some(io_err) = err.chain_any::<io::Error>() {
1004    ///         println!("Found IO error in chain: {:?}", io_err.kind());
1005    ///     }
1006    /// }
1007    /// ```
1008    #[cfg(feature = "std")]
1009    pub fn chain_any<T: StdError + 'static>(&self) -> Option<&T> {
1010        // First check the root error
1011        if let Some(e) = self.source.downcast_ref::<T>() {
1012            return Some(e);
1013        }
1014
1015        // Walk the source's cause chain (for wrapped errors with causes)
1016        let mut current: Option<&(dyn StdError + 'static)> = self.source.as_dyn_error().source();
1017        while let Some(err) = current {
1018            // Direct type match
1019            if let Some(e) = err.downcast_ref::<T>() {
1020                return Some(e);
1021            }
1022
1023            // If it's a Handled<Error>, recursively search inside it
1024            if let Some(handled) = err.downcast_ref::<Handled<Error>>() {
1025                if let Some(e) = handled.chain_any::<T>() {
1026                    return Some(e);
1027                }
1028            }
1029
1030            current = err.source();
1031        }
1032
1033        // Check the chained previous errors (from chain_after)
1034        if let Some(ref chained) = self.chained {
1035            if let Some(e) = chained.chain_any::<T>() {
1036                return Some(e);
1037            }
1038        }
1039
1040        None
1041    }
1042
1043    /// Find all errors of type `T` in the cause chain.
1044    ///
1045    /// Walks the error chain via `std::error::Error::source()` and collects
1046    /// references to all errors that match type `T`.
1047    ///
1048    /// # Example
1049    ///
1050    /// ```
1051    /// use handle_this::{Handled, Result};
1052    /// use std::io;
1053    ///
1054    /// fn check_all(err: &Handled) {
1055    ///     let io_errors = err.chain_all::<io::Error>();
1056    ///     for e in io_errors {
1057    ///         println!("IO error: {:?}", e.kind());
1058    ///     }
1059    /// }
1060    /// ```
1061    #[cfg(feature = "std")]
1062    pub fn chain_all<T: StdError + 'static>(&self) -> Vec<&T> {
1063        let mut matches = Vec::new();
1064
1065        // First check the root error
1066        if let Some(e) = self.source.downcast_ref::<T>() {
1067            matches.push(e);
1068        }
1069
1070        // Walk the source's cause chain (for wrapped errors with causes)
1071        let mut current: Option<&(dyn StdError + 'static)> = self.source.as_dyn_error().source();
1072        while let Some(err) = current {
1073            // If it's a Handled<Error>, recursively search inside it
1074            if let Some(handled) = err.downcast_ref::<Handled<Error>>() {
1075                matches.extend(handled.chain_all::<T>());
1076                break; // Handled contains everything, nothing more to walk
1077            }
1078
1079            // Direct type match
1080            if let Some(e) = err.downcast_ref::<T>() {
1081                matches.push(e);
1082            }
1083
1084            current = err.source();
1085        }
1086
1087        // Check the chained previous errors (from chain_after)
1088        if let Some(ref chained) = self.chained {
1089            matches.extend(chained.chain_all::<T>());
1090        }
1091
1092        matches
1093    }
1094}
1095
1096// Note: We intentionally do NOT have a generic `From<E> for Handled<E>` impl
1097// because it would conflict with `From<T> for T` when E is already a Handled.
1098// Instead, the __try_block! macro produces the raw error type, and wrapping
1099// happens at the macro boundary.
1100
1101// ============================================================
1102// Display and Error implementations
1103// ============================================================
1104
1105impl<E: fmt::Display> fmt::Display for Handled<E> {
1106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1107        let msg = self.message.get_or_init(|| self.source.to_string());
1108        writeln!(f, "{}", msg)?;
1109
1110        if !self.locations.is_empty() {
1111            writeln!(f, "\nTrace (most recent last):")?;
1112            for (idx, loc) in self.locations.iter().enumerate() {
1113                write!(f, "  {}:{}:{}", loc.file, loc.line, loc.col)?;
1114
1115                // Find context for this location if any
1116                if let Some(contexts) = &self.contexts {
1117                    if let Some(ctx) = contexts.iter().find(|c| c.location_idx == idx as u16) {
1118                        if let Some(msg) = &ctx.message {
1119                            write!(f, "\n    \u{2192} {}", msg)?;
1120                        }
1121                        for (k, v) in &ctx.attachments {
1122                            write!(f, "\n    {}: {}", k, v)?;
1123                        }
1124                    }
1125                }
1126                writeln!(f)?;
1127            }
1128        }
1129
1130        Ok(())
1131    }
1132}
1133
1134// Note: We intentionally do NOT have a generic `impl<E: Error> Error for Handled<E>`.
1135// This prevents `Handled<E>` from satisfying the `E: Error` bound in `IntoHandled`,
1136// which allows us to have non-conflicting trait impls for wrapping.
1137
1138// StdError impl ONLY for type-erased Handled (needed for ? operator in functions returning Result<_, Handled>)
1139#[cfg(feature = "std")]
1140impl StdError for Handled<Error> {
1141    fn source(&self) -> Option<&(dyn StdError + 'static)> {
1142        Some(self.source.as_dyn_error())
1143    }
1144}
1145
1146// ============================================================
1147// wrap_any - wraps errors avoiding double-wrap using TypeId
1148// ============================================================
1149
1150/// Wrap any error in Handled, avoiding double-wrapping.
1151/// Uses TypeId to detect if the input is already a Handled<Error>.
1152#[doc(hidden)]
1153#[cfg(feature = "std")]
1154pub fn __wrap_any<E: StdError + Send + Sync + 'static>(e: E) -> Handled<Error> {
1155    use core::any::TypeId;
1156    // Check if E is Handled<Error>
1157    if TypeId::of::<E>() == TypeId::of::<Handled<Error>>() {
1158        // SAFETY: TypeId guarantees E is Handled<Error>
1159        unsafe {
1160            let handled = core::ptr::read(&e as *const E as *const Handled<Error>);
1161            core::mem::forget(e);
1162            handled
1163        }
1164    } else {
1165        Handled::wrap(e)
1166    }
1167}
1168
1169// ============================================================
1170// From impls for type-erased Handled
1171// ============================================================
1172
1173impl From<&str> for Handled<Error> {
1174    fn from(s: &str) -> Self {
1175        Self::msg(s)
1176    }
1177}
1178
1179impl From<String> for Handled<Error> {
1180    fn from(s: String) -> Self {
1181        Self::msg(s)
1182    }
1183}
1184
1185// Primitive type impls - allow using numbers as error codes
1186macro_rules! impl_from_primitive {
1187    ($($t:ty),*) => {
1188        $(
1189            impl From<$t> for Handled<Error> {
1190                fn from(v: $t) -> Self {
1191                    Self::msg(v.to_string())
1192                }
1193            }
1194        )*
1195    };
1196}
1197
1198impl_from_primitive!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);
1199
1200// Unit type - used as default error type for try catch inference
1201impl From<()> for Handled<Error> {
1202    fn from(_: ()) -> Self {
1203        Self::msg("error")
1204    }
1205}
1206
1207// Box<dyn Display> - allows any Display type to convert to Handled
1208#[cfg(feature = "std")]
1209impl From<Box<dyn core::fmt::Display + Send + Sync>> for Handled<Error> {
1210    fn from(e: Box<dyn core::fmt::Display + Send + Sync>) -> Self {
1211        Self::msg(e.to_string())
1212    }
1213}
1214
1215#[cfg(feature = "std")]
1216impl From<Box<dyn StdError + Send + Sync + 'static>> for Handled<Error> {
1217    fn from(e: Box<dyn StdError + Send + Sync + 'static>) -> Self {
1218        Self::wrap_box(e)
1219    }
1220}
1221
1222// Specific From impls for common error types.
1223// We can't use a blanket impl because Handled<Error> itself implements Error,
1224// which would conflict with From<T> for T.
1225
1226#[cfg(feature = "std")]
1227impl From<std::io::Error> for Handled<Error> {
1228    fn from(e: std::io::Error) -> Self {
1229        Self::wrap(e)
1230    }
1231}
1232
1233#[cfg(feature = "std")]
1234impl From<std::num::ParseIntError> for Handled<Error> {
1235    fn from(e: std::num::ParseIntError) -> Self {
1236        Self::wrap(e)
1237    }
1238}
1239
1240#[cfg(feature = "std")]
1241impl From<std::num::ParseFloatError> for Handled<Error> {
1242    fn from(e: std::num::ParseFloatError) -> Self {
1243        Self::wrap(e)
1244    }
1245}
1246
1247#[cfg(feature = "std")]
1248impl From<std::str::Utf8Error> for Handled<Error> {
1249    fn from(e: std::str::Utf8Error) -> Self {
1250        Self::wrap(e)
1251    }
1252}
1253
1254#[cfg(feature = "std")]
1255impl From<std::string::FromUtf8Error> for Handled<Error> {
1256    fn from(e: std::string::FromUtf8Error) -> Self {
1257        Self::wrap(e)
1258    }
1259}
1260
1261// Note: We intentionally don't have a generic From<Handled<E>> for Handled<Error>
1262// as it conflicts with the blanket From<T> for T, causing type inference issues.
1263// Use the .erase() method for explicit conversion instead.
1264
1265// ============================================================
1266// anyhow interop
1267// ============================================================
1268
1269#[cfg(feature = "anyhow")]
1270impl From<anyhow::Error> for Handled<Error> {
1271    fn from(e: anyhow::Error) -> Self {
1272        match e.downcast::<Handled<Error>>() {
1273            Ok(h) => h,
1274            Err(e) => {
1275                let msg = e.to_string();
1276                let mut handled = Self::msg(msg);
1277                for (i, cause) in e.chain().skip(1).enumerate() {
1278                    handled = handled
1279                        .frame("<anyhow>", i as u32, 0)
1280                        .ctx(cause.to_string());
1281                }
1282                handled
1283            }
1284        }
1285    }
1286}
1287
1288// ============================================================
1289// eyre interop
1290// ============================================================
1291
1292#[cfg(feature = "eyre")]
1293impl From<eyre::Report> for Handled<Error> {
1294    fn from(e: eyre::Report) -> Self {
1295        match e.downcast::<Handled<Error>>() {
1296            Ok(h) => h,
1297            Err(e) => {
1298                let msg = e.to_string();
1299                let mut handled = Self::msg(msg);
1300                for (i, cause) in e.chain().skip(1).enumerate() {
1301                    handled = handled
1302                        .frame("<eyre>", i as u32, 0)
1303                        .ctx(cause.to_string());
1304                }
1305                handled
1306            }
1307        }
1308    }
1309}
1310
1311// ============================================================
1312// Serde support
1313// ============================================================
1314
1315#[cfg(feature = "serde")]
1316mod serde_impl {
1317    use super::*;
1318    #[cfg(feature = "std")]
1319    use std::collections::BTreeMap;
1320    #[cfg(not(feature = "std"))]
1321    use alloc::collections::BTreeMap;
1322    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1323
1324    // Serialize Value to preserve type information
1325    impl Serialize for Value {
1326        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1327            match self {
1328                Value::String(s) => serializer.serialize_str(s),
1329                Value::Int(n) => serializer.serialize_i64(*n),
1330                Value::Uint(n) => serializer.serialize_u64(*n),
1331                Value::Float(n) => serializer.serialize_f64(*n),
1332                Value::Bool(b) => serializer.serialize_bool(*b),
1333                Value::Null => serializer.serialize_none(),
1334            }
1335        }
1336    }
1337
1338    impl<'de> Deserialize<'de> for Value {
1339        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1340            use serde::de::{self, Visitor};
1341
1342            struct ValueVisitor;
1343
1344            impl<'de> Visitor<'de> for ValueVisitor {
1345                type Value = Value;
1346
1347                fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1348                    formatter.write_str("a string, number, boolean, or null")
1349                }
1350
1351                fn visit_bool<E: de::Error>(self, v: bool) -> Result<Value, E> {
1352                    Ok(Value::Bool(v))
1353                }
1354
1355                fn visit_i64<E: de::Error>(self, v: i64) -> Result<Value, E> {
1356                    Ok(Value::Int(v))
1357                }
1358
1359                fn visit_u64<E: de::Error>(self, v: u64) -> Result<Value, E> {
1360                    Ok(Value::Uint(v))
1361                }
1362
1363                fn visit_f64<E: de::Error>(self, v: f64) -> Result<Value, E> {
1364                    Ok(Value::Float(v))
1365                }
1366
1367                fn visit_str<E: de::Error>(self, v: &str) -> Result<Value, E> {
1368                    Ok(Value::String(v.to_string()))
1369                }
1370
1371                fn visit_string<E: de::Error>(self, v: String) -> Result<Value, E> {
1372                    Ok(Value::String(v))
1373                }
1374
1375                fn visit_none<E: de::Error>(self) -> Result<Value, E> {
1376                    Ok(Value::Null)
1377                }
1378
1379                fn visit_unit<E: de::Error>(self) -> Result<Value, E> {
1380                    Ok(Value::Null)
1381                }
1382            }
1383
1384            deserializer.deserialize_any(ValueVisitor)
1385        }
1386    }
1387
1388    #[derive(Serialize, Deserialize)]
1389    struct SerializedFrame {
1390        file: String,
1391        line: u32,
1392        col: u32,
1393        #[serde(skip_serializing_if = "Option::is_none")]
1394        message: Option<String>,
1395        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1396        attachments: BTreeMap<String, Value>,
1397    }
1398
1399    #[derive(Serialize, Deserialize)]
1400    struct SerializedHandled {
1401        message: String,
1402        trace: Vec<SerializedFrame>,
1403    }
1404
1405    // Only implement for Error variant (type-erased)
1406    impl Serialize for Handled<Error> {
1407        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1408            let contexts = self.contexts.as_ref();
1409            let serialized = SerializedHandled {
1410                message: self.message().to_string(),
1411                trace: self
1412                    .locations
1413                    .iter()
1414                    .enumerate()
1415                    .map(|(idx, loc)| {
1416                        let ctx = contexts.and_then(|c| c.iter().find(|e| e.location_idx == idx as u16));
1417                        SerializedFrame {
1418                            file: loc.file.to_string(),
1419                            line: loc.line,
1420                            col: loc.col,
1421                            message: ctx.and_then(|c| c.message.clone()),
1422                            attachments: ctx
1423                                .map(|c| c.attachments.iter()
1424                                    .map(|(k, v)| (k.to_string(), v.clone()))
1425                                    .collect::<BTreeMap<_, _>>())
1426                                .unwrap_or_default(),
1427                        }
1428                    })
1429                    .collect(),
1430            };
1431            serialized.serialize(serializer)
1432        }
1433    }
1434
1435    impl<'de> Deserialize<'de> for Handled<Error> {
1436        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1437            let serialized = SerializedHandled::deserialize(deserializer)?;
1438
1439            let mut locations = LocationVec::new();
1440            let mut contexts = Vec::new();
1441
1442            for (idx, f) in serialized.trace.into_iter().enumerate() {
1443                // Note: deserialized files are owned strings, we leak them to get 'static
1444                // This is acceptable for deserialized errors which are typically short-lived
1445                let file: &'static str = Box::leak(f.file.into_boxed_str());
1446                locations.push(Location {
1447                    file,
1448                    line: f.line,
1449                    col: f.col,
1450                });
1451
1452                if f.message.is_some() || !f.attachments.is_empty() {
1453                    contexts.push(ContextEntry {
1454                        location_idx: idx as u16,
1455                        message: f.message,
1456                        attachments: f.attachments
1457                            .into_iter()
1458                            .map(|(k, v)| (Cow::Owned(k), v))
1459                            .collect(),
1460                    });
1461                }
1462            }
1463
1464            Ok(Self {
1465                message: {
1466                    let lock = OnceLock::new();
1467                    let _ = lock.set(serialized.message.clone());
1468                    lock
1469                },
1470                source: Error::new(StringError(serialized.message)),
1471                locations,
1472                contexts: if contexts.is_empty() { None } else { Some(contexts) },
1473                #[cfg(feature = "std")]
1474                chained: None,
1475            })
1476        }
1477    }
1478
1479    impl Serialize for Location {
1480        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1481            SerializedFrame {
1482                file: self.file.to_string(),
1483                line: self.line,
1484                col: self.col,
1485                message: None,
1486                attachments: BTreeMap::new(),
1487            }
1488            .serialize(serializer)
1489        }
1490    }
1491
1492    // Custom serializer for FrameView that serializes attachments as a map
1493    impl Serialize for FrameView<'_> {
1494        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1495            use serde::ser::SerializeStruct;
1496            let mut state = serializer.serialize_struct("FrameView", 5)?;
1497            state.serialize_field("file", self.file)?;
1498            state.serialize_field("line", &self.line)?;
1499            state.serialize_field("col", &self.col)?;
1500            if self.context.is_some() {
1501                state.serialize_field("message", &self.context)?;
1502            }
1503            if !self.attachments_inner.is_empty() {
1504                // Serialize as a map
1505                let map: BTreeMap<&str, &Value> = self.attachments_inner
1506                    .iter()
1507                    .map(|(k, v)| (k.as_ref(), v))
1508                    .collect();
1509                state.serialize_field("attachments", &map)?;
1510            }
1511            state.end()
1512        }
1513    }
1514}