Skip to main content

error_stack/
sink.rs

1#[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
2use core::panic::Location;
3#[cfg(nightly)]
4use core::{
5    convert::Infallible,
6    ops::{FromResidual, Try},
7};
8
9use crate::Report;
10
11/// The `Bomb` type is used to enforce proper usage of `ReportSink` at runtime.
12///
13/// It addresses a limitation of the `#[must_use]` attribute, which becomes ineffective
14/// when methods like `&mut self` are called, marking the value as used prematurely.
15///
16/// By moving this check to runtime, `Bomb` ensures that `ReportSink` is properly
17/// consumed.
18///
19/// This runtime check complements the compile-time `#[must_use]` attribute,
20/// providing a more robust mechanism to prevent `ReportSink` not being consumed.
21#[derive(Debug)]
22enum BombState {
23    /// Panic if the `ReportSink` is dropped without being used.
24    Panic,
25    /// Emit a warning to stderr if the `ReportSink` is dropped without being used.
26    Warn(
27        // We capture the location if either `tracing` is enabled or `std` is enabled on non-WASM
28        // targets.
29        #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
30        &'static Location<'static>,
31    ),
32    /// Do nothing if the `ReportSink` is properly consumed.
33    Defused,
34}
35
36impl Default for BombState {
37    #[track_caller]
38    fn default() -> Self {
39        Self::Warn(
40            #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
41            Location::caller(),
42        )
43    }
44}
45
46#[derive(Debug, Default)]
47struct Bomb(BombState);
48
49impl Bomb {
50    const fn panic() -> Self {
51        Self(BombState::Panic)
52    }
53
54    #[track_caller]
55    const fn warn() -> Self {
56        Self(BombState::Warn(
57            #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
58            Location::caller(),
59        ))
60    }
61
62    const fn defuse(&mut self) {
63        self.0 = BombState::Defused;
64    }
65}
66
67impl Drop for Bomb {
68    fn drop(&mut self) {
69        // If we're in release mode, we don't need to do anything
70        if !cfg!(debug_assertions) {
71            return;
72        }
73
74        match self.0 {
75            BombState::Panic => panic!("ReportSink was dropped without being consumed"),
76            #[cfg_attr(not(feature = "tracing"), expect(clippy::print_stderr))]
77            #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
78            BombState::Warn(location) => {
79                #[cfg(feature = "tracing")]
80                tracing::warn!(
81                    target: "error_stack",
82                    %location,
83                    "`ReportSink` was dropped without being consumed"
84                );
85                #[cfg(not(feature = "tracing"))]
86                eprintln!("`ReportSink` was dropped without being consumed at {location}");
87            }
88            _ => {}
89        }
90    }
91}
92/// A sink for collecting multiple [`Report`]s into a single [`Result`].
93///
94/// [`ReportSink`] allows you to accumulate multiple errors or reports and then
95/// finalize them into a single `Result`. This is particularly useful when you
96/// need to collect errors from multiple operations before deciding whether to
97/// proceed or fail.
98///
99/// The sink is equipped with a "bomb" mechanism to ensure proper usage,
100/// if the sink hasn't been finished when dropped, it will emit a warning or panic,
101/// depending on the constructor used.
102///
103/// # Examples
104///
105/// ```
106/// use error_stack::{Report, ReportSink};
107///
108/// #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
109/// struct InternalError;
110///
111/// impl core::fmt::Display for InternalError {
112///     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
113///         f.write_str("Internal error")
114///     }
115/// }
116///
117/// impl core::error::Error for InternalError {}
118///
119/// fn operation1() -> Result<u32, Report<InternalError>> {
120///     // ...
121///     # Ok(42)
122/// }
123///
124/// fn operation2() -> Result<(), Report<InternalError>> {
125///     // ...
126///     # Ok(())
127/// }
128///
129/// fn process_data() -> Result<(), Report<[InternalError]>> {
130///     let mut sink = ReportSink::new();
131///
132///     if let Some(value) = sink.attempt(operation1()) {
133///         // process value
134///         # let _value = value;
135///     }
136///
137///     if let Err(e) = operation2() {
138///         sink.append(e);
139///     }
140///
141///     sink.finish()
142/// }
143/// # let _result = process_data();
144/// ```
145#[must_use]
146pub struct ReportSink<C> {
147    report: Option<Report<[C]>>,
148    bomb: Bomb,
149}
150
151impl<C> ReportSink<C> {
152    /// Creates a new [`ReportSink`].
153    ///
154    /// If the sink hasn't been finished when dropped, it will emit a warning.
155    #[track_caller]
156    pub const fn new() -> Self {
157        Self {
158            report: None,
159            bomb: Bomb::warn(),
160        }
161    }
162
163    /// Creates a new [`ReportSink`].
164    ///
165    /// If the sink hasn't been finished when dropped, it will panic.
166    pub const fn new_armed() -> Self {
167        Self {
168            report: None,
169            bomb: Bomb::panic(),
170        }
171    }
172
173    /// Adds a [`Report`] to the sink.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// # use error_stack::{ReportSink, Report};
179    /// # use std::io;
180    /// let mut sink = ReportSink::new();
181    /// sink.append(Report::new(io::Error::new(
182    ///     io::ErrorKind::Other,
183    ///     "I/O error",
184    /// )));
185    /// ```
186    #[track_caller]
187    pub fn append(&mut self, report: impl Into<Report<[C]>>) {
188        let report = report.into();
189
190        match self.report.as_mut() {
191            Some(existing) => existing.append(report),
192            None => self.report = Some(report),
193        }
194    }
195
196    /// Captures a single error or report in the sink.
197    ///
198    /// This method is similar to [`append`], but allows for bare errors without prior [`Report`]
199    /// creation.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// # use error_stack::ReportSink;
205    /// # use std::io;
206    /// let mut sink = ReportSink::new();
207    /// sink.capture(io::Error::new(io::ErrorKind::Other, "I/O error"));
208    /// ```
209    ///
210    /// [`append`]: ReportSink::append
211    #[track_caller]
212    pub fn capture(&mut self, error: impl Into<Report<C>>) {
213        let report = error.into();
214
215        match self.report.as_mut() {
216            Some(existing) => existing.push(report),
217            None => self.report = Some(report.into()),
218        }
219    }
220
221    /// Attempts to execute a fallible operation and collect any errors.
222    ///
223    /// This method takes a [`Result`] and returns an [`Option`]:
224    /// - If the [`Result`] is [`Ok`], it returns <code>[Some]\(T)</code> with the successful value.
225    /// - If the [`Result`] is [`Err`], it captures the error in the sink and returns [`None`].
226    ///
227    /// This is useful for concisely handling operations that may fail, allowing you to
228    /// collect errors while continuing execution.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// # use error_stack::ReportSink;
234    /// # use std::io;
235    /// fn fallible_operation() -> Result<u32, io::Error> {
236    ///     // ...
237    ///     # Ok(42)
238    /// }
239    ///
240    /// let mut sink = ReportSink::new();
241    /// let value = sink.attempt(fallible_operation());
242    /// if let Some(v) = value {
243    ///     // Use the successful value
244    ///     # let _v = v;
245    /// }
246    /// // Any errors are now collected in the sink
247    /// # let _result = sink.finish();
248    /// ```
249    #[track_caller]
250    pub fn attempt<T, R>(&mut self, result: Result<T, R>) -> Option<T>
251    where
252        R: Into<Report<C>>,
253    {
254        match result {
255            Ok(value) => Some(value),
256            Err(error) => {
257                self.capture(error);
258                None
259            }
260        }
261    }
262
263    /// Finishes the sink and returns a [`Result`].
264    ///
265    /// This method consumes the sink, and returns `Ok(())` if no errors
266    /// were collected, or `Err(Report<[C]>)` containing all collected errors otherwise.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// # use error_stack::ReportSink;
272    /// # use std::io;
273    /// let mut sink = ReportSink::new();
274    /// # // needed for type inference
275    /// # sink.capture(io::Error::new(io::ErrorKind::Other, "I/O error"));
276    /// // ... add errors ...
277    /// let result = sink.finish();
278    /// # let _result = result;
279    /// ```
280    pub fn finish(mut self) -> Result<(), Report<[C]>> {
281        self.bomb.defuse();
282        self.report.map_or(Ok(()), Err)
283    }
284
285    /// Finishes the sink and returns a [`Result`] with a custom success value.
286    ///
287    /// Similar to [`finish`], but allows specifying a function to generate the success value.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// # use error_stack::ReportSink;
293    /// # use std::io;
294    /// let mut sink = ReportSink::new();
295    /// # // needed for type inference
296    /// # sink.capture(io::Error::new(io::ErrorKind::Other, "I/O error"));
297    /// // ... add errors ...
298    /// let result = sink.finish_with(|| "Operation completed");
299    /// # let _result = result;
300    /// ```
301    ///
302    /// [`finish`]: ReportSink::finish
303    pub fn finish_with<T>(mut self, ok: impl FnOnce() -> T) -> Result<T, Report<[C]>> {
304        self.bomb.defuse();
305        self.report.map_or_else(|| Ok(ok()), Err)
306    }
307
308    /// Finishes the sink and returns a [`Result`] with a default success value.
309    ///
310    /// Similar to [`finish`], but uses `T::default()` as the success value.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// # use error_stack::ReportSink;
316    /// # use std::io;
317    /// let mut sink = ReportSink::new();
318    /// # // needed for type inference
319    /// # sink.capture(io::Error::new(io::ErrorKind::Other, "I/O error"));
320    /// // ... add errors ...
321    /// let result: Result<Vec<String>, _> = sink.finish_default();
322    /// # let _result = result;
323    /// ```
324    ///
325    /// [`finish`]: ReportSink::finish
326    pub fn finish_default<T: Default>(mut self) -> Result<T, Report<[C]>> {
327        self.bomb.defuse();
328        self.report.map_or_else(|| Ok(T::default()), Err)
329    }
330
331    /// Finishes the sink and returns a [`Result`] with a provided success value.
332    ///
333    /// Similar to [`finish`], but allows specifying a concrete value for the success case.
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// # use error_stack::ReportSink;
339    /// # use std::io;
340    /// let mut sink = ReportSink::new();
341    /// # // needed for type inference
342    /// # sink.capture(io::Error::new(io::ErrorKind::Other, "I/O error"));
343    /// // ... add errors ...
344    /// let result = sink.finish_ok(42);
345    /// # let _result = result;
346    /// ```
347    ///
348    /// [`finish`]: ReportSink::finish
349    pub fn finish_ok<T>(mut self, ok: T) -> Result<T, Report<[C]>> {
350        self.bomb.defuse();
351        self.report.map_or(Ok(ok), Err)
352    }
353}
354
355impl<C> Default for ReportSink<C> {
356    fn default() -> Self {
357        Self::new()
358    }
359}
360
361#[cfg(nightly)]
362impl<C> FromResidual for ReportSink<C> {
363    fn from_residual(residual: <Self as Try>::Residual) -> Self {
364        match residual {
365            Err(report) => Self {
366                report: Some(report),
367                bomb: Bomb::default(),
368            },
369        }
370    }
371}
372
373#[cfg(nightly)]
374impl<C> Try for ReportSink<C> {
375    type Output = ();
376    // needs to be infallible, not `!` because of the `Try` of `Result`
377    type Residual = Result<Infallible, Report<[C]>>;
378
379    fn from_output((): ()) -> Self {
380        Self {
381            report: None,
382            bomb: Bomb::default(),
383        }
384    }
385
386    fn branch(mut self) -> core::ops::ControlFlow<Self::Residual, Self::Output> {
387        self.bomb.defuse();
388        self.report.map_or(
389            core::ops::ControlFlow::Continue(()), //
390            |report| core::ops::ControlFlow::Break(Err(report)),
391        )
392    }
393}
394
395#[cfg(test)]
396mod test {
397    use alloc::collections::BTreeSet;
398    use core::fmt::Display;
399
400    use crate::{Report, sink::ReportSink};
401
402    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
403    struct TestError(u8);
404
405    impl Display for TestError {
406        fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
407            fmt.write_str("TestError(")?;
408            core::fmt::Display::fmt(&self.0, fmt)?;
409            fmt.write_str(")")
410        }
411    }
412
413    impl core::error::Error for TestError {}
414
415    #[test]
416    fn add_single() {
417        let mut sink = ReportSink::new();
418
419        sink.append(Report::new(TestError(0)));
420
421        let report = sink.finish().expect_err("should have failed");
422
423        let contexts: BTreeSet<_> = report.current_contexts().collect();
424        assert_eq!(contexts.len(), 1);
425        assert!(contexts.contains(&TestError(0)));
426    }
427
428    #[test]
429    fn add_multiple() {
430        let mut sink = ReportSink::new();
431
432        sink.append(Report::new(TestError(0)));
433        sink.append(Report::new(TestError(1)));
434
435        let report = sink.finish().expect_err("should have failed");
436
437        let contexts: BTreeSet<_> = report.current_contexts().collect();
438        assert_eq!(contexts.len(), 2);
439        assert!(contexts.contains(&TestError(0)));
440        assert!(contexts.contains(&TestError(1)));
441    }
442
443    #[test]
444    fn capture_single() {
445        let mut sink = ReportSink::new();
446
447        sink.capture(TestError(0));
448
449        let report = sink.finish().expect_err("should have failed");
450
451        let contexts: BTreeSet<_> = report.current_contexts().collect();
452        assert_eq!(contexts.len(), 1);
453        assert!(contexts.contains(&TestError(0)));
454    }
455
456    #[test]
457    fn capture_multiple() {
458        let mut sink = ReportSink::new();
459
460        sink.capture(TestError(0));
461        sink.capture(TestError(1));
462
463        let report = sink.finish().expect_err("should have failed");
464
465        let contexts: BTreeSet<_> = report.current_contexts().collect();
466        assert_eq!(contexts.len(), 2);
467        assert!(contexts.contains(&TestError(0)));
468        assert!(contexts.contains(&TestError(1)));
469    }
470
471    #[test]
472    fn new_does_not_panic() {
473        let _sink: ReportSink<TestError> = ReportSink::new();
474    }
475
476    #[cfg(nightly)]
477    #[test]
478    fn try_none() {
479        fn sink() -> Result<(), Report<[TestError]>> {
480            let sink = ReportSink::new();
481
482            sink?;
483
484            Ok(())
485        }
486
487        sink().expect("should not have failed");
488    }
489
490    #[cfg(nightly)]
491    #[test]
492    fn try_single() {
493        fn sink() -> Result<(), Report<[TestError]>> {
494            let mut sink = ReportSink::new();
495
496            sink.append(Report::new(TestError(0)));
497
498            sink?;
499            Ok(())
500        }
501
502        let report = sink().expect_err("should have failed");
503
504        let contexts: BTreeSet<_> = report.current_contexts().collect();
505        assert_eq!(contexts.len(), 1);
506        assert!(contexts.contains(&TestError(0)));
507    }
508
509    #[cfg(nightly)]
510    #[test]
511    fn try_multiple() {
512        fn sink() -> Result<(), Report<[TestError]>> {
513            let mut sink = ReportSink::new();
514
515            sink.append(Report::new(TestError(0)));
516            sink.append(Report::new(TestError(1)));
517
518            sink?;
519            Ok(())
520        }
521
522        let report = sink().expect_err("should have failed");
523
524        let contexts: BTreeSet<_> = report.current_contexts().collect();
525        assert_eq!(contexts.len(), 2);
526        assert!(contexts.contains(&TestError(0)));
527        assert!(contexts.contains(&TestError(1)));
528    }
529
530    #[cfg(nightly)]
531    #[test]
532    fn try_arbitrary_return() {
533        fn sink() -> Result<u8, Report<[TestError]>> {
534            let mut sink = ReportSink::new();
535
536            sink.append(Report::new(TestError(0)));
537
538            sink?;
539            Ok(8)
540        }
541
542        let report = sink().expect_err("should have failed");
543
544        let contexts: BTreeSet<_> = report.current_contexts().collect();
545        assert_eq!(contexts.len(), 1);
546        assert!(contexts.contains(&TestError(0)));
547    }
548
549    #[test]
550    #[should_panic(expected = "without being consumed")]
551    fn panic_on_unused() {
552        #[expect(clippy::unnecessary_wraps)]
553        fn sink() -> Result<(), Report<[TestError]>> {
554            let mut sink = ReportSink::new_armed();
555
556            sink.append(Report::new(TestError(0)));
557
558            Ok(())
559        }
560
561        let _result = sink();
562    }
563
564    #[test]
565    fn panic_on_unused_with_defuse() {
566        fn sink() -> Result<(), Report<[TestError]>> {
567            let mut sink = ReportSink::new_armed();
568
569            sink.append(Report::new(TestError(0)));
570
571            sink?;
572            Ok(())
573        }
574
575        let report = sink().expect_err("should have failed");
576
577        let contexts: BTreeSet<_> = report.current_contexts().collect();
578        assert_eq!(contexts.len(), 1);
579        assert!(contexts.contains(&TestError(0)));
580    }
581
582    #[test]
583    fn finish() {
584        let mut sink = ReportSink::new();
585
586        sink.append(Report::new(TestError(0)));
587        sink.append(Report::new(TestError(1)));
588
589        let report = sink.finish().expect_err("should have failed");
590
591        let contexts: BTreeSet<_> = report.current_contexts().collect();
592        assert_eq!(contexts.len(), 2);
593        assert!(contexts.contains(&TestError(0)));
594        assert!(contexts.contains(&TestError(1)));
595    }
596
597    #[test]
598    fn finish_ok() {
599        let sink: ReportSink<TestError> = ReportSink::new();
600
601        sink.finish().expect("should have succeeded");
602    }
603
604    #[test]
605    fn finish_with() {
606        let mut sink = ReportSink::new();
607
608        sink.append(Report::new(TestError(0)));
609        sink.append(Report::new(TestError(1)));
610
611        let report = sink.finish_with(|| 8).expect_err("should have failed");
612
613        let contexts: BTreeSet<_> = report.current_contexts().collect();
614        assert_eq!(contexts.len(), 2);
615        assert!(contexts.contains(&TestError(0)));
616        assert!(contexts.contains(&TestError(1)));
617    }
618
619    #[test]
620    fn finish_with_ok() {
621        let sink: ReportSink<TestError> = ReportSink::new();
622
623        let value = sink.finish_with(|| 8).expect("should have succeeded");
624        assert_eq!(value, 8);
625    }
626
627    #[test]
628    fn finish_default() {
629        let mut sink = ReportSink::new();
630
631        sink.append(Report::new(TestError(0)));
632        sink.append(Report::new(TestError(1)));
633
634        let report = sink.finish_default::<u8>().expect_err("should have failed");
635
636        let contexts: BTreeSet<_> = report.current_contexts().collect();
637        assert_eq!(contexts.len(), 2);
638        assert!(contexts.contains(&TestError(0)));
639        assert!(contexts.contains(&TestError(1)));
640    }
641
642    #[test]
643    fn finish_default_ok() {
644        let sink: ReportSink<TestError> = ReportSink::new();
645
646        let value = sink.finish_default::<u8>().expect("should have succeeded");
647        assert_eq!(value, 0);
648    }
649
650    #[test]
651    fn finish_with_value() {
652        let mut sink = ReportSink::new();
653
654        sink.append(Report::new(TestError(0)));
655        sink.append(Report::new(TestError(1)));
656
657        let report = sink.finish_ok(8).expect_err("should have failed");
658
659        let contexts: BTreeSet<_> = report.current_contexts().collect();
660        assert_eq!(contexts.len(), 2);
661        assert!(contexts.contains(&TestError(0)));
662        assert!(contexts.contains(&TestError(1)));
663    }
664
665    #[test]
666    fn finish_with_value_ok() {
667        let sink: ReportSink<TestError> = ReportSink::new();
668
669        let value = sink.finish_ok(8).expect("should have succeeded");
670        assert_eq!(value, 8);
671    }
672}