thread_checked_lock/
error.rs

1use std::{convert::Infallible, error::Error, sync::PoisonError};
2use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
3
4
5/// Extension trait for [`Result`] which adds the ability to more conveniently handle the poison
6/// errors returned by this crate's locks.
7///
8/// ## About Poison
9///
10/// When another thread panics while it holds a poisonable lock, the lock becomes poisoned (which
11/// may be manually cleared). Attempts to acquire a poisoned lock (or otherwise access, via the
12/// lock, the data protected by the lock) return poison errors, which act as speed bumps for
13/// accessing data whose logical invariants are potentially broken. See `std`'s [`PoisonError`] for
14/// more.
15///
16/// As a poison error is only returned when some thread has already panicked, it is common to
17/// unconditionally panic in the current thread as well when poison is encountered, or to simply
18/// ignore such a circumstance.
19///
20/// As a notable example, [`parking_lot`] does not provide poison errors at all, and does not care
21/// whether a different thread panicked while holding a [`parking_lot`] mutex. This is roughly
22/// equivalent to (but more performant than) using [`HandlePoisonResult::ignore_poison`]
23/// everywhere.
24///
25///
26/// [`parking_lot`]: https://docs.rs/parking_lot/
27pub trait HandlePoisonResult {
28    /// A variation of the `Self` result type which cannot possibly be a poison error.
29    type PoisonlessResult;
30
31    /// Silently converts any poison error into a successful result (see
32    /// [`PoisonError::into_inner`]), and otherwise returns the result unchanged.
33    ///
34    /// [Read more about poison.](HandlePoisonResult#about-poison)
35    #[must_use]
36    fn ignore_poison(self) -> Self::PoisonlessResult;
37
38    /// Panics if the result was caused by poison, and otherwise returns the result unchanged.
39    ///
40    /// # Panics
41    /// Panics if the result is an [`Err`] that was caused by poison.
42    ///
43    /// [Read more about poison.](HandlePoisonResult#about-poison)
44    fn panic_if_poison(self) -> Self::PoisonlessResult;
45}
46
47/// Helper function to coerce an uninhabited poison error into `!`.
48#[inline]
49fn prove_unreachable(poison: &PoisonError<Infallible>) -> ! {
50    #[expect(clippy::uninhabited_references, reason = "this function is not reachable")]
51    match *poison.get_ref() {}
52}
53
54
55/// The result type returned by [`ThreadCheckedMutex::lock`].
56///
57/// [`ThreadCheckedMutex::lock`]: super::mutex::ThreadCheckedMutex::lock
58pub type LockResult<T> = Result<T, LockError<T>>;
59/// A variation of [`LockResult<T>`] which cannot possibly be a poison error.
60pub type PoisonlessLockResult<T> = Result<T, LockError<Infallible>>;
61
62impl<T> HandlePoisonResult for LockResult<T> {
63    type PoisonlessResult = PoisonlessLockResult<T>;
64
65    /// Silently converts any poison error into a successful result (see
66    /// [`PoisonError::into_inner`]), and otherwise returns the result unchanged.
67    ///
68    /// [Read more about poison.](HandlePoisonResult#about-poison)
69    #[inline]
70    fn ignore_poison(self) -> Self::PoisonlessResult {
71        match self.map_err(LockError::ignore_poison) {
72            Ok(t)                  => Ok(t),
73            Err(poisonless_result) => poisonless_result,
74        }
75    }
76
77    /// Panics if the result was caused by poison, and otherwise returns the result unchanged.
78    ///
79    /// # Panics
80    /// Panics if the result is an [`Err`] that was caused by poison.
81    ///
82    /// [Read more about poison.](HandlePoisonResult#about-poison)
83    #[inline]
84    fn panic_if_poison(self) -> Self::PoisonlessResult {
85        self.map_err(LockError::panic_if_poison)
86    }
87}
88
89/// An error that may be returned by [`ThreadCheckedMutex::lock`].
90///
91/// [`ThreadCheckedMutex::lock`]: super::mutex::ThreadCheckedMutex::lock
92pub enum LockError<T> {
93    /// Returned when a lock was acquired, but the lock was poisoned.
94    ///
95    /// [Read more about poison.](HandlePoisonResult#about-poison)
96    Poisoned(PoisonError<T>),
97    /// Returned when a lock failed to be acquired because the thread attempting to acquire
98    /// the lock was already holding the lock.
99    LockedByCurrentThread,
100}
101
102impl<T> LockError<T> {
103    /// Silently converts any poison error into a successful result (see
104    /// [`PoisonError::into_inner`]), and otherwise returns the error unchanged in an [`Err`].
105    ///
106    /// [Read more about poison.](HandlePoisonResult#about-poison)
107    ///
108    /// # Errors
109    /// If the provided error was not caused by poison, that error is returned.
110    #[inline]
111    pub fn ignore_poison(self) -> PoisonlessLockResult<T> {
112        match self {
113            Self::Poisoned(poison)      => Ok(poison.into_inner()),
114            Self::LockedByCurrentThread => Err(LockError::LockedByCurrentThread),
115        }
116    }
117
118    /// Panics if the error was caused by poison, and otherwise returns the error unchanged.
119    ///
120    /// # Panics
121    /// Panics if the error was caused by poison.
122    ///
123    /// [Read more about poison.](HandlePoisonResult#about-poison)
124    #[inline]
125    #[must_use]
126    pub fn panic_if_poison(self) -> LockError<Infallible> {
127        match self {
128            #[expect(
129                clippy::panic,
130                reason = "library users will frequently want to panic on poison",
131            )]
132            Self::Poisoned(_)           => panic!("LockError was poison"),
133            Self::LockedByCurrentThread => LockError::LockedByCurrentThread,
134        }
135    }
136}
137
138impl<T> From<PoisonError<T>> for LockError<T> {
139    #[inline]
140    fn from(poison: PoisonError<T>) -> Self {
141        Self::Poisoned(poison)
142    }
143}
144
145impl<T> Debug for LockError<T> {
146    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
147        match self {
148            Self::Poisoned(poison)      => f.debug_tuple("Poisoned").field(&poison).finish(),
149            Self::LockedByCurrentThread => f.write_str("LockedByCurrentThread"),
150        }
151    }
152}
153
154impl<T> Display for LockError<T> {
155    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
156        match self {
157            Self::Poisoned(_) => write!(
158                f,
159                "LockError due to poison (another thread panicked)",
160            ),
161            Self::LockedByCurrentThread => write!(
162                f,
163                "Failed to acquire a lock, because the same thread was holding it",
164            ),
165        }
166    }
167}
168
169impl<T> Error for LockError<T> {}
170
171impl PartialEq for LockError<Infallible> {
172    #[inline]
173    fn eq(&self, _other: &Self) -> bool {
174        // There's only one inhabited variant of `LockError<Infallible>`, so this returns true.
175        match self {
176            Self::LockedByCurrentThread => true,
177            Self::Poisoned(poison)      => prove_unreachable(poison),
178        }
179    }
180}
181
182impl Eq for LockError<Infallible> {}
183
184
185/// The result type returned by [`ThreadCheckedMutex::try_lock`].
186///
187/// [`ThreadCheckedMutex::try_lock`]: super::mutex::ThreadCheckedMutex::try_lock
188pub type TryLockResult<T> = Result<T, TryLockError<T>>;
189/// A variation of [`TryLockResult<T>`] which cannot possibly be a poison error.
190pub type PoisonlessTryLockResult<T> = Result<T, TryLockError<Infallible>>;
191
192impl<T> HandlePoisonResult for TryLockResult<T> {
193    type PoisonlessResult = PoisonlessTryLockResult<T>;
194
195    /// Silently converts any poison error into a successful result (see
196    /// [`PoisonError::into_inner`]), and otherwise returns the result unchanged.
197    ///
198    /// [Read more about poison.](HandlePoisonResult#about-poison)
199    #[inline]
200    fn ignore_poison(self) -> Self::PoisonlessResult {
201        match self.map_err(TryLockError::ignore_poison) {
202            Ok(t)                  => Ok(t),
203            Err(poisonless_result) => poisonless_result,
204        }
205    }
206
207    /// Panics if the result was caused by poison, and otherwise returns the result unchanged.
208    ///
209    /// # Panics
210    /// Panics if the result is an [`Err`] that was caused by poison.
211    ///
212    /// [Read more about poison.](HandlePoisonResult#about-poison)
213    #[inline]
214    fn panic_if_poison(self) -> Self::PoisonlessResult {
215        self.map_err(TryLockError::panic_if_poison)
216    }
217}
218
219/// An error that may be returned by [`ThreadCheckedMutex::try_lock`].
220///
221/// [`ThreadCheckedMutex::try_lock`]: super::mutex::ThreadCheckedMutex::try_lock
222pub enum TryLockError<T> {
223    /// Returned when a lock was acquired, but the lock was poisoned.
224    ///
225    /// [Read more about poison.](HandlePoisonResult#about-poison)
226    Poisoned(PoisonError<T>),
227    /// Returned when a lock failed to be acquired because the thread attempting to acquire
228    /// the lock was already holding the lock.
229    LockedByCurrentThread,
230    /// Returned when a lock failed to be acquired because the lock was already held by a thread
231    /// (other than the thread attempting to acquire the lock).
232    WouldBlock,
233}
234
235impl<T> TryLockError<T> {
236    /// Silently converts any poison error into a successful result (see
237    /// [`PoisonError::into_inner`]), and otherwise returns the error unchanged in an [`Err`].
238    ///
239    /// [Read more about poison.](HandlePoisonResult#about-poison)
240    ///
241    /// # Errors
242    ///
243    /// If the provided error was not caused by poison, that error is returned.
244    #[inline]
245    pub fn ignore_poison(self) -> PoisonlessTryLockResult<T> {
246        match self {
247            Self::Poisoned(poison)      => Ok(poison.into_inner()),
248            Self::LockedByCurrentThread => Err(TryLockError::LockedByCurrentThread),
249            Self::WouldBlock            => Err(TryLockError::WouldBlock),
250        }
251    }
252
253    /// Panics if the error was caused by poison, and otherwise returns the error unchanged.
254    ///
255    /// # Panics
256    /// Panics if the error was caused by poison.
257    ///
258    /// [Read more about poison.](HandlePoisonResult#about-poison)
259    #[inline]
260    #[must_use]
261    pub fn panic_if_poison(self) -> TryLockError<Infallible> {
262        match self {
263            #[expect(
264                clippy::panic,
265                reason = "library users will frequently want to panic on poison",
266            )]
267            Self::Poisoned(_)           => panic!("TryLockError was poison"),
268            Self::LockedByCurrentThread => TryLockError::LockedByCurrentThread,
269            Self::WouldBlock            => TryLockError::WouldBlock,
270        }
271    }
272}
273
274impl<T> From<PoisonError<T>> for TryLockError<T> {
275    #[inline]
276    fn from(poison: PoisonError<T>) -> Self {
277        Self::Poisoned(poison)
278    }
279}
280
281impl<T> Debug for TryLockError<T> {
282    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
283        match self {
284            Self::Poisoned(poison)      => f.debug_tuple("Poisoned").field(&poison).finish(),
285            Self::LockedByCurrentThread => f.write_str("LockedByCurrentThread"),
286            Self::WouldBlock            => f.write_str("WouldBlock"),
287        }
288    }
289}
290
291impl<T> Display for TryLockError<T> {
292    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
293        match self {
294            Self::Poisoned(_) => write!(
295                f,
296                "TryLockError due to poison (another thread panicked)",
297            ),
298            Self::LockedByCurrentThread => write!(
299                f,
300                "Failed to acquire a lock, because the same thread was holding it",
301            ),
302            Self::WouldBlock => write!(
303                f,
304                "Lock was held by a different thread, so acquiring it would block",
305            ),
306        }
307    }
308}
309
310impl<T> Error for TryLockError<T> {}
311
312impl PartialEq for TryLockError<Infallible> {
313    #[inline]
314    fn eq(&self, other: &Self) -> bool {
315        match self {
316            Self::LockedByCurrentThread => matches!(other, Self::LockedByCurrentThread),
317            Self::WouldBlock            => matches!(other, Self::WouldBlock),
318            Self::Poisoned(poison)      => prove_unreachable(poison),
319        }
320    }
321}
322
323impl Eq for TryLockError<Infallible> {}
324
325
326/// The result type returned by [`ThreadCheckedMutex::into_inner`] or
327/// [`ThreadCheckedMutex::get_mut`].
328///
329/// [`ThreadCheckedMutex::into_inner`]: super::mutex::ThreadCheckedMutex::into_inner
330/// [`ThreadCheckedMutex::get_mut`]: super::mutex::ThreadCheckedMutex::get_mut
331pub type AccessResult<T> = Result<T, AccessError<T>>;
332/// A variation of [`AccessResult<T>`] which cannot possibly be a poison error.
333///
334/// Note that every [`AccessError`] is caused by poison, so this result is always [`Ok`].
335pub type PoisonlessAccessResult<T> = Result<T, AccessError<Infallible>>;
336
337impl<T> HandlePoisonResult for AccessResult<T> {
338    type PoisonlessResult = PoisonlessAccessResult<T>;
339
340    /// Silently converts any poison error into a successful result (see
341    /// [`PoisonError::into_inner`]), and otherwise returns the result unchanged.
342    ///
343    /// Since every [`AccessError`] is caused by poison, the returned result is always [`Ok`].
344    ///
345    /// [Read more about poison.](HandlePoisonResult#about-poison)
346    #[inline]
347    fn ignore_poison(self) -> Self::PoisonlessResult {
348        match self.map_err(AccessError::ignore_poison) {
349            Ok(t)                  => Ok(t),
350            Err(poisonless_result) => poisonless_result,
351        }
352    }
353
354    /// Panics if the error was caused by poison, and otherwise returns the error unchanged.
355    ///
356    /// Note that every [`AccessError`] is caused by poison, so this is similar to unwrapping the
357    /// result.
358    ///
359    /// # Panics
360    /// Panics if the result is an [`Err`], which was necessarily caused by poison.
361    ///
362    /// [Read more about poison.](HandlePoisonResult#about-poison)
363    #[inline]
364    fn panic_if_poison(self) -> Self::PoisonlessResult {
365        self.map_err(|err| AccessError::panic_if_poison(err))
366    }
367}
368
369/// Returned when a lock's data was accessed, but the lock was poisoned.
370///
371/// [Read more about poison.](HandlePoisonResult#about-poison)
372///
373/// This error may be returned by [`ThreadCheckedMutex::into_inner`] or
374/// [`ThreadCheckedMutex::get_mut`].
375///
376/// [`ThreadCheckedMutex::into_inner`]: super::mutex::ThreadCheckedMutex::into_inner
377/// [`ThreadCheckedMutex::get_mut`]: super::mutex::ThreadCheckedMutex::get_mut
378pub struct AccessError<T> {
379    /// The only possible cause of an `AccessError` is a poisoned lock.
380    pub poison: PoisonError<T>,
381}
382
383impl<T> AccessError<T> {
384    /// Silently converts any poison error into a successful result (see
385    /// [`PoisonError::into_inner`]).
386    ///
387    /// Since every [`AccessError`] is caused by poison, the returned result is always [`Ok`].
388    ///
389    /// [Read more about poison.](HandlePoisonResult#about-poison)
390    #[expect(clippy::missing_errors_doc, reason = "the function is infallible")]
391    #[inline]
392    pub fn ignore_poison(self) -> PoisonlessAccessResult<T> {
393        Ok(self.poison.into_inner())
394    }
395
396    /// Panics if the [`AccessError`] was caused by poison, which is always the case; this function
397    /// always panics.
398    ///
399    /// # Panics
400    /// Panics unconditionally, as the error is necessarily caused by poison.
401    ///
402    /// [Read more about poison.](HandlePoisonResult#about-poison)
403    #[inline]
404    pub fn panic_if_poison(self) -> ! {
405        #![expect(
406            clippy::panic,
407            reason = "library users will frequently want to panic on poison",
408        )]
409        panic!("AccessError is poison")
410    }
411}
412
413impl<T> From<PoisonError<T>> for AccessError<T> {
414    #[inline]
415    fn from(poison: PoisonError<T>) -> Self {
416        Self { poison }
417    }
418}
419
420impl<T> Debug for AccessError<T> {
421    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
422        f.debug_struct("AccessError")
423            .field("poison", &self.poison)
424            .finish()
425    }
426}
427
428impl<T> Display for AccessError<T> {
429    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
430        write!(f, "AccessError due to poison (another thread panicked)")
431    }
432}
433
434impl<T> Error for AccessError<T> {}
435
436impl PartialEq for AccessError<Infallible> {
437    #[inline]
438    fn eq(&self, _other: &Self) -> bool {
439        prove_unreachable(&self.poison)
440    }
441}
442
443impl Eq for AccessError<Infallible> {}
444
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn lock_ignore_poison() {
452        // Ok
453        let res_o: LockResult<()> = Ok(());
454        assert!(matches!(res_o.ignore_poison(), Ok(())));
455
456        // Err but not poison
457        let res_e: LockResult<()> = Err(LockError::LockedByCurrentThread);
458        assert!(matches!(res_e.ignore_poison(), Err(LockError::LockedByCurrentThread)));
459
460        // Poison
461        let res_p: LockResult<()> = Err(PoisonError::new(()).into());
462        assert!(matches!(res_p.ignore_poison(), Ok(())));
463    }
464
465    #[test]
466    fn lock_panic_if_poison() {
467        // Ok
468        let res_o: LockResult<()> = Ok(());
469        assert!(matches!(res_o.panic_if_poison(), Ok(())));
470
471        // Err but not poison
472        let res_e: LockResult<()> = Err(LockError::LockedByCurrentThread);
473        assert!(matches!(res_e.panic_if_poison(), Err(LockError::LockedByCurrentThread)));
474    }
475
476    #[test]
477    #[should_panic = "LockError was poison"]
478    fn panicking_lock_panic_if_poison() {
479        // Poison
480        let res_p: LockResult<()> = Err(PoisonError::new(()).into());
481        #[expect(
482            clippy::let_underscore_must_use,
483            clippy::let_underscore_untyped,
484            reason = "function never returns",
485        )]
486        let _ = res_p.panic_if_poison();
487    }
488
489    #[test]
490    fn try_lock_ignore_poison() {
491        // Ok
492        let res_o: TryLockResult<()> = Ok(());
493        assert!(matches!(res_o.ignore_poison(), Ok(())));
494
495        // Err but not poison
496        let res_e: TryLockResult<()> = Err(TryLockError::LockedByCurrentThread);
497        assert!(matches!(res_e.ignore_poison(), Err(TryLockError::LockedByCurrentThread)));
498
499        // Poison
500        let res_p: TryLockResult<()> = Err(PoisonError::new(()).into());
501        assert!(matches!(res_p.ignore_poison(), Ok(())));
502    }
503
504    #[test]
505    fn try_lock_panic_if_poison() {
506        // Ok
507        let res_o: TryLockResult<()> = Ok(());
508        assert!(matches!(res_o.panic_if_poison(), Ok(())));
509
510        // Err but not poison
511        let res_e: TryLockResult<()> = Err(TryLockError::LockedByCurrentThread);
512        assert!(matches!(res_e.panic_if_poison(), Err(TryLockError::LockedByCurrentThread)));
513    }
514
515    #[test]
516    #[should_panic = "TryLockError was poison"]
517    fn panicking_try_lock_panic_if_poison() {
518        // Poison
519        let res_p: TryLockResult<()> = Err(PoisonError::new(()).into());
520        #[expect(
521            clippy::let_underscore_must_use,
522            clippy::let_underscore_untyped,
523            reason = "function never returns",
524        )]
525        let _ = res_p.panic_if_poison();
526    }
527
528    #[test]
529    fn access_ignore_poison() {
530        // Ok
531        let res_o: AccessResult<()> = Ok(());
532        assert!(matches!(res_o.ignore_poison(), Ok(())));
533
534        // Err but not poison.. is impossible.
535
536        // Poison
537        let res_p: AccessResult<()> = Err(PoisonError::new(()).into());
538        assert!(matches!(res_p.ignore_poison(), Ok(())));
539    }
540
541    #[test]
542    fn access_panic_if_poison() {
543        // Ok
544        let res_o: AccessResult<()> = Ok(());
545        assert!(matches!(res_o.panic_if_poison(), Ok(())));
546
547        // Err but not poison.. is impossible.
548    }
549
550    #[test]
551    #[should_panic = "AccessError is poison"]
552    fn panicking_access_panic_if_poison() {
553        // Poison
554        let res_p: AccessResult<()> = Err(PoisonError::new(()).into());
555        #[expect(
556            clippy::let_underscore_must_use,
557            clippy::let_underscore_untyped,
558            reason = "function never returns",
559        )]
560        let _ = res_p.panic_if_poison();
561    }
562
563    fn test_eq_impl<E: Eq, const N: usize>(errors: &[E; N]) {
564        for (i, error) in errors.iter().enumerate() {
565            for (j, other) in errors.iter().enumerate() {
566                assert_eq!(i == j, error == other);
567            }
568        }
569    }
570
571    #[test]
572    fn eq_impls() {
573        // The `::<Infallible>`s are not strictly necessary, but make it more clear.
574        test_eq_impl(&[
575            LockError::<Infallible>::LockedByCurrentThread,
576        ]);
577        test_eq_impl(&[
578            TryLockError::<Infallible>::LockedByCurrentThread,
579            TryLockError::<Infallible>::WouldBlock,
580        ]);
581        // `AccessError<Infallible>` is uninhabited.
582    }
583}