jiff/error/mod.rs
1use crate::util::{
2 b::{BoundsError, SpecialBoundsError},
3 sync::Arc,
4};
5
6pub(crate) mod civil;
7pub(crate) mod duration;
8pub(crate) mod fmt;
9pub(crate) mod signed_duration;
10pub(crate) mod span;
11pub(crate) mod timestamp;
12pub(crate) mod tz;
13pub(crate) mod unit;
14pub(crate) mod util;
15pub(crate) mod zoned;
16
17/// An error that can occur in this crate.
18///
19/// The most common type of error is a result of overflow. But other errors
20/// exist as well:
21///
22/// * Time zone database lookup failure.
23/// * Configuration problem. (For example, trying to round a span with calendar
24/// units without providing a relative datetime.)
25/// * An I/O error as a result of trying to open a time zone database from a
26/// directory via
27/// [`TimeZoneDatabase::from_dir`](crate::tz::TimeZoneDatabase::from_dir).
28/// * Parse errors.
29///
30/// # Introspection is limited
31///
32/// Other than implementing the [`std::error::Error`] trait when the
33/// `std` feature is enabled, the [`core::fmt::Debug`] trait and the
34/// [`core::fmt::Display`] trait, this error type currently provides
35/// very limited introspection capabilities. Simple predicates like
36/// `Error::is_range` are provided, but the predicates are not
37/// exhaustive. That is, there exist some errors that do not return
38/// `true` for any of the `Error::is_*` predicates.
39///
40/// # Design
41///
42/// This crate follows the "One True God Error Type Pattern," where only one
43/// error type exists for a variety of different operations. This design was
44/// chosen after attempting to provide finer grained error types. But finer
45/// grained error types proved difficult in the face of composition.
46///
47/// More about this design choice can be found in a GitHub issue
48/// [about error types].
49///
50/// [about error types]: https://github.com/BurntSushi/jiff/issues/8
51#[derive(Clone)]
52pub struct Error {
53 /// The internal representation of an error.
54 ///
55 /// This is in an `Arc` to make an `Error` cloneable. It could otherwise
56 /// be automatically cloneable, but it embeds a `std::io::Error` when the
57 /// `std` feature is enabled, which isn't cloneable.
58 ///
59 /// This also makes clones cheap. And it also make the size of error equal
60 /// to one word (although a `Box` would achieve that last goal). This is
61 /// why we put the `Arc` here instead of on `std::io::Error` directly.
62 inner: Option<Arc<ErrorInner>>,
63}
64
65#[derive(Debug)]
66#[cfg_attr(not(feature = "alloc"), derive(Clone))]
67struct ErrorInner {
68 kind: ErrorKind,
69 #[cfg(feature = "alloc")]
70 cause: Option<Error>,
71}
72
73impl Error {
74 /// Creates a new error value from `core::fmt::Arguments`.
75 ///
76 /// It is expected to use [`format_args!`](format_args) from
77 /// Rust's standard library (available in `core`) to create a
78 /// `core::fmt::Arguments`.
79 ///
80 /// Callers should generally use their own error types. But in some
81 /// circumstances, it can be convenient to manufacture a Jiff error value
82 /// specifically.
83 ///
84 /// # Core-only environments
85 ///
86 /// In core-only environments without a dynamic memory allocator, error
87 /// messages may be degraded in some cases. For example, if the given
88 /// `core::fmt::Arguments` could not be converted to a simple borrowed
89 /// `&str`, then this will ignore the input given and return an "unknown"
90 /// Jiff error.
91 ///
92 /// # Example
93 ///
94 /// ```
95 /// use jiff::Error;
96 ///
97 /// let err = Error::from_args(format_args!("something failed"));
98 /// assert_eq!(err.to_string(), "something failed");
99 /// ```
100 pub fn from_args<'a>(message: core::fmt::Arguments<'a>) -> Error {
101 Error::from(ErrorKind::Adhoc(AdhocError::from_args(message)))
102 }
103
104 /// Returns true when this error originated as a result of a value being
105 /// out of Jiff's supported range.
106 ///
107 /// # Example
108 ///
109 /// ```
110 /// use jiff::civil::Date;
111 ///
112 /// assert!(Date::new(2025, 2, 29).unwrap_err().is_range());
113 /// assert!("2025-02-29".parse::<Date>().unwrap_err().is_range());
114 /// assert!(Date::strptime("%Y-%m-%d", "2025-02-29").unwrap_err().is_range());
115 /// ```
116 pub fn is_range(&self) -> bool {
117 use self::ErrorKind::*;
118 matches!(
119 *self.root().kind(),
120 Bounds(_) | SpecialBounds(_) | ITimeRange(_)
121 )
122 }
123
124 /// Returns true when this error originated as a result of an invalid
125 /// configuration of parameters to a function call.
126 ///
127 /// This particular error category is somewhat nebulous, but it's generally
128 /// meant to cover errors that _could_ have been statically prevented by
129 /// Jiff with more types in its API. Instead, a smaller API is preferred.
130 ///
131 /// # Example: invalid rounding options
132 ///
133 /// ```
134 /// use jiff::{SpanRound, ToSpan, Unit};
135 ///
136 /// let span = 44.seconds();
137 /// let err = span.round(
138 /// SpanRound::new().smallest(Unit::Second).increment(45),
139 /// ).unwrap_err();
140 /// // Rounding increments for seconds must divide evenly into `60`.
141 /// // But `45` does not. Thus, this is a "configuration" error.
142 /// assert!(err.is_invalid_parameter());
143 /// ```
144 ///
145 /// # Example: invalid units
146 ///
147 /// One cannot round a span between dates to units less than days:
148 ///
149 /// ```
150 /// use jiff::{civil::date, Unit};
151 ///
152 /// let date1 = date(2025, 3, 18);
153 /// let date2 = date(2025, 12, 21);
154 /// let err = date1.until((Unit::Hour, date2)).unwrap_err();
155 /// assert!(err.is_invalid_parameter());
156 /// ```
157 ///
158 /// Similarly, one cannot round a span between times to units greater than
159 /// hours:
160 ///
161 /// ```
162 /// use jiff::{civil::time, Unit};
163 ///
164 /// let time1 = time(9, 39, 0, 0);
165 /// let time2 = time(17, 0, 0, 0);
166 /// let err = time1.until((Unit::Day, time2)).unwrap_err();
167 /// assert!(err.is_invalid_parameter());
168 /// ```
169 pub fn is_invalid_parameter(&self) -> bool {
170 use self::civil::Error as CivilError;
171 use self::ErrorKind::*;
172
173 matches!(
174 *self.root().kind(),
175 UnitConfig(_)
176 | Civil(
177 CivilError::IllegalTimeWithMicrosecond
178 | CivilError::IllegalTimeWithMillisecond
179 | CivilError::IllegalTimeWithNanosecond
180 )
181 )
182 }
183
184 /// Returns true when this error originated as a result of an operation
185 /// failing because an appropriate Jiff crate feature was not enabled.
186 ///
187 /// # Example
188 ///
189 /// ```ignore
190 /// use jiff::tz::TimeZone;
191 ///
192 /// // This passes when the `tz-system` crate feature is NOT enabled.
193 /// assert!(TimeZone::try_system().unwrap_err().is_crate_feature());
194 /// ```
195 pub fn is_crate_feature(&self) -> bool {
196 matches!(*self.root().kind(), ErrorKind::CrateFeature(_))
197 }
198}
199
200impl Error {
201 #[inline(never)]
202 #[cold]
203 pub(crate) fn bounds(err: BoundsError) -> Error {
204 Error::from(ErrorKind::Bounds(err))
205 }
206
207 #[inline(never)]
208 #[cold]
209 pub(crate) fn special_bounds(err: SpecialBoundsError) -> Error {
210 Error::from(ErrorKind::SpecialBounds(err))
211 }
212
213 /// Creates a new error from the special "shared" error type.
214 pub(crate) fn itime_range(
215 err: crate::shared::util::itime::RangeError,
216 ) -> Error {
217 Error::from(ErrorKind::ITimeRange(err))
218 }
219
220 /// Creates a new error from the special TZif error type.
221 #[cfg(feature = "alloc")]
222 pub(crate) fn tzif(err: crate::shared::tzif::TzifError) -> Error {
223 Error::from(ErrorKind::Tzif(err))
224 }
225
226 /// Creates a new error from the special `PosixTimeZoneError` type.
227 pub(crate) fn posix_tz(
228 err: crate::shared::posix::PosixTimeZoneError,
229 ) -> Error {
230 Error::from(ErrorKind::PosixTz(err))
231 }
232
233 /// A convenience constructor for building an I/O error.
234 ///
235 /// This returns an error that is just a simple wrapper around the
236 /// `std::io::Error` type. In general, callers should always attach some
237 /// kind of context to this error (like a file path).
238 ///
239 /// This is only available when the `std` feature is enabled.
240 #[cfg(feature = "std")]
241 #[inline(never)]
242 #[cold]
243 pub(crate) fn io(err: std::io::Error) -> Error {
244 Error::from(ErrorKind::IO(IOError { err }))
245 }
246
247 /// Contextualizes this error by associating the given file path with it.
248 ///
249 /// This is a convenience routine for calling `Error::context` with a
250 /// `FilePathError`.
251 #[cfg(any(feature = "tzdb-zoneinfo", feature = "tzdb-concatenated"))]
252 #[inline(never)]
253 #[cold]
254 pub(crate) fn path(self, path: impl Into<std::path::PathBuf>) -> Error {
255 let err = Error::from(ErrorKind::FilePath(FilePathError {
256 path: path.into(),
257 }));
258 self.context(err)
259 }
260
261 /*
262 /// Creates a new "unknown" Jiff error.
263 ///
264 /// The benefit of this API is that it permits creating an `Error` in a
265 /// `const` context. But the error message quality is currently pretty
266 /// bad: it's just a generic "unknown Jiff error" message.
267 ///
268 /// This could be improved to take a `&'static str`, but I believe this
269 /// will require pointer tagging in order to avoid increasing the size of
270 /// `Error`. (Which is important, because of how many perf sensitive
271 /// APIs return a `Result<T, Error>` in Jiff.
272 pub(crate) const fn unknown() -> Error {
273 Error { inner: None }
274 }
275 */
276
277 #[cfg_attr(feature = "perf-inline", inline(always))]
278 pub(crate) fn context(self, consequent: impl IntoError) -> Error {
279 self.context_impl(consequent.into_error())
280 }
281
282 #[inline(never)]
283 #[cold]
284 fn context_impl(self, _consequent: Error) -> Error {
285 #[cfg(feature = "alloc")]
286 {
287 let mut err = _consequent;
288 if err.inner.is_none() {
289 err = Error::from(ErrorKind::Unknown);
290 }
291 let inner = err.inner.as_mut().unwrap();
292 assert!(
293 inner.cause.is_none(),
294 "cause of consequence must be `None`"
295 );
296 // OK because we just created this error so the Arc
297 // has one reference.
298 Arc::get_mut(inner).unwrap().cause = Some(self);
299 err
300 }
301 #[cfg(not(feature = "alloc"))]
302 {
303 // We just completely drop `self`. :-(
304 //
305 // 2025-12-21: ... actually, we used to drop self, but this
306 // ends up dropping the root cause. And the root cause
307 // is how the predicates on `Error` work. So we drop the
308 // consequent instead.
309 self
310 }
311 }
312
313 /// Returns the root error in this chain.
314 fn root(&self) -> &Error {
315 // OK because `Error::chain` is guaranteed to return a non-empty
316 // iterator.
317 self.chain().last().unwrap()
318 }
319
320 /// Returns a chain of error values.
321 ///
322 /// This starts with the most recent error added to the chain. That is,
323 /// the highest level context. The last error in the chain is always the
324 /// "root" cause. That is, the error closest to the point where something
325 /// has gone wrong.
326 ///
327 /// The iterator returned is guaranteed to yield at least one error.
328 fn chain(&self) -> impl Iterator<Item = &Error> {
329 #[cfg(feature = "alloc")]
330 {
331 let mut err = self;
332 core::iter::once(err).chain(core::iter::from_fn(move || {
333 err = err
334 .inner
335 .as_ref()
336 .and_then(|inner| inner.cause.as_ref())?;
337 Some(err)
338 }))
339 }
340 #[cfg(not(feature = "alloc"))]
341 {
342 core::iter::once(self)
343 }
344 }
345
346 /// Returns the kind of this error.
347 fn kind(&self) -> &ErrorKind {
348 self.inner
349 .as_ref()
350 .map(|inner| &inner.kind)
351 .unwrap_or(&ErrorKind::Unknown)
352 }
353}
354
355#[cfg(feature = "std")]
356impl std::error::Error for Error {}
357
358impl core::fmt::Display for Error {
359 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
360 let mut it = self.chain().peekable();
361 while let Some(err) = it.next() {
362 core::fmt::Display::fmt(err.kind(), f)?;
363 if it.peek().is_some() {
364 f.write_str(": ")?;
365 }
366 }
367 Ok(())
368 }
369}
370
371impl core::fmt::Debug for Error {
372 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
373 if !f.alternate() {
374 core::fmt::Display::fmt(self, f)
375 } else {
376 let Some(ref inner) = self.inner else {
377 return f
378 .debug_struct("Error")
379 .field("kind", &"None")
380 .finish();
381 };
382 #[cfg(feature = "alloc")]
383 {
384 f.debug_struct("Error")
385 .field("kind", &inner.kind)
386 .field("cause", &inner.cause)
387 .finish()
388 }
389 #[cfg(not(feature = "alloc"))]
390 {
391 f.debug_struct("Error").field("kind", &inner.kind).finish()
392 }
393 }
394 }
395}
396
397/// The underlying kind of a [`Error`].
398#[derive(Debug)]
399#[cfg_attr(not(feature = "alloc"), derive(Clone))]
400enum ErrorKind {
401 Adhoc(AdhocError),
402 Bounds(BoundsError),
403 Civil(self::civil::Error),
404 CrateFeature(CrateFeatureError),
405 Duration(self::duration::Error),
406 #[allow(dead_code)] // not used in some feature configs
407 FilePath(FilePathError),
408 Fmt(self::fmt::Error),
409 FmtFriendly(self::fmt::friendly::Error),
410 FmtOffset(self::fmt::offset::Error),
411 FmtRfc2822(self::fmt::rfc2822::Error),
412 FmtRfc9557(self::fmt::rfc9557::Error),
413 FmtTemporal(self::fmt::temporal::Error),
414 FmtUtil(self::fmt::util::Error),
415 FmtStrtime(self::fmt::strtime::Error),
416 FmtStrtimeFormat(self::fmt::strtime::FormatError),
417 FmtStrtimeParse(self::fmt::strtime::ParseError),
418 #[allow(dead_code)] // not used in some feature configs
419 IO(IOError),
420 ITimeRange(crate::shared::util::itime::RangeError),
421 OsStrUtf8(self::util::OsStrUtf8Error),
422 ParseInt(self::util::ParseIntError),
423 ParseFraction(self::util::ParseFractionError),
424 PosixTz(crate::shared::posix::PosixTimeZoneError),
425 RoundingIncrement(self::util::RoundingIncrementError),
426 SignedDuration(self::signed_duration::Error),
427 Span(self::span::Error),
428 UnitConfig(self::unit::UnitConfigError),
429 SpecialBounds(SpecialBoundsError),
430 Timestamp(self::timestamp::Error),
431 TzAmbiguous(self::tz::ambiguous::Error),
432 TzDb(self::tz::db::Error),
433 TzConcatenated(self::tz::concatenated::Error),
434 TzOffset(self::tz::offset::Error),
435 TzPosix(self::tz::posix::Error),
436 TzSystem(self::tz::system::Error),
437 TzTimeZone(self::tz::timezone::Error),
438 #[allow(dead_code)]
439 TzZic(self::tz::zic::Error),
440 #[cfg(feature = "alloc")]
441 Tzif(crate::shared::tzif::TzifError),
442 Unknown,
443 Zoned(self::zoned::Error),
444}
445
446impl core::fmt::Display for ErrorKind {
447 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
448 use self::ErrorKind::*;
449
450 match *self {
451 Adhoc(ref msg) => msg.fmt(f),
452 Bounds(ref msg) => msg.fmt(f),
453 Civil(ref err) => err.fmt(f),
454 CrateFeature(ref err) => err.fmt(f),
455 Duration(ref err) => err.fmt(f),
456 FilePath(ref err) => err.fmt(f),
457 Fmt(ref err) => err.fmt(f),
458 FmtFriendly(ref err) => err.fmt(f),
459 FmtOffset(ref err) => err.fmt(f),
460 FmtRfc2822(ref err) => err.fmt(f),
461 FmtRfc9557(ref err) => err.fmt(f),
462 FmtUtil(ref err) => err.fmt(f),
463 FmtStrtime(ref err) => err.fmt(f),
464 FmtStrtimeFormat(ref err) => err.fmt(f),
465 FmtStrtimeParse(ref err) => err.fmt(f),
466 FmtTemporal(ref err) => err.fmt(f),
467 IO(ref err) => err.fmt(f),
468 ITimeRange(ref err) => err.fmt(f),
469 OsStrUtf8(ref err) => err.fmt(f),
470 ParseInt(ref err) => err.fmt(f),
471 ParseFraction(ref err) => err.fmt(f),
472 PosixTz(ref err) => err.fmt(f),
473 RoundingIncrement(ref err) => err.fmt(f),
474 SignedDuration(ref err) => err.fmt(f),
475 Span(ref err) => err.fmt(f),
476 UnitConfig(ref err) => err.fmt(f),
477 SpecialBounds(ref msg) => msg.fmt(f),
478 Timestamp(ref err) => err.fmt(f),
479 TzAmbiguous(ref err) => err.fmt(f),
480 TzDb(ref err) => err.fmt(f),
481 TzConcatenated(ref err) => err.fmt(f),
482 TzOffset(ref err) => err.fmt(f),
483 TzPosix(ref err) => err.fmt(f),
484 TzSystem(ref err) => err.fmt(f),
485 TzTimeZone(ref err) => err.fmt(f),
486 TzZic(ref err) => err.fmt(f),
487 #[cfg(feature = "alloc")]
488 Tzif(ref err) => err.fmt(f),
489 Unknown => f.write_str("unknown Jiff error"),
490 Zoned(ref err) => err.fmt(f),
491 }
492 }
493}
494
495impl From<ErrorKind> for Error {
496 fn from(kind: ErrorKind) -> Error {
497 #[cfg(feature = "alloc")]
498 {
499 Error { inner: Some(Arc::new(ErrorInner { kind, cause: None })) }
500 }
501 #[cfg(not(feature = "alloc"))]
502 {
503 Error { inner: Some(Arc::new(ErrorInner { kind })) }
504 }
505 }
506}
507
508/// A generic error message.
509///
510/// This used to be used to represent most errors in Jiff. But then I switched
511/// to more structured error types (internally). We still keep this around to
512/// support the `Error::from_args` public API, which permits users of Jiff to
513/// manifest their own `Error` values from an arbitrary message.
514#[cfg_attr(not(feature = "alloc"), derive(Clone))]
515struct AdhocError {
516 #[cfg(feature = "alloc")]
517 message: alloc::boxed::Box<str>,
518 #[cfg(not(feature = "alloc"))]
519 message: &'static str,
520}
521
522impl AdhocError {
523 fn from_args<'a>(message: core::fmt::Arguments<'a>) -> AdhocError {
524 #[cfg(feature = "alloc")]
525 {
526 use alloc::string::ToString;
527
528 let message = message.to_string().into_boxed_str();
529 AdhocError { message }
530 }
531 #[cfg(not(feature = "alloc"))]
532 {
533 let message = message.as_str().unwrap_or(
534 "unknown Jiff error (better error messages require \
535 enabling the `alloc` feature for the `jiff` crate)",
536 );
537 AdhocError { message }
538 }
539 }
540}
541
542#[cfg(feature = "std")]
543impl std::error::Error for AdhocError {}
544
545impl core::fmt::Display for AdhocError {
546 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
547 core::fmt::Display::fmt(&self.message, f)
548 }
549}
550
551impl core::fmt::Debug for AdhocError {
552 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
553 core::fmt::Debug::fmt(&self.message, f)
554 }
555}
556
557/// An error used whenever a failure is caused by a missing crate feature.
558///
559/// This enum doesn't necessarily contain every Jiff crate feature. It only
560/// contains the features whose absence can result in an error.
561#[derive(Clone, Debug)]
562pub(crate) enum CrateFeatureError {
563 #[cfg(not(feature = "tz-system"))]
564 TzSystem,
565 #[cfg(not(feature = "tzdb-concatenated"))]
566 TzdbConcatenated,
567 #[cfg(not(feature = "tzdb-zoneinfo"))]
568 TzdbZoneInfo,
569}
570
571impl From<CrateFeatureError> for Error {
572 #[cold]
573 #[inline(never)]
574 fn from(err: CrateFeatureError) -> Error {
575 ErrorKind::CrateFeature(err).into()
576 }
577}
578
579impl core::fmt::Display for CrateFeatureError {
580 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
581 #[allow(unused_imports)]
582 use self::CrateFeatureError::*;
583
584 f.write_str("operation failed because Jiff crate feature `")?;
585 #[allow(unused_variables)]
586 let name: &str = match *self {
587 #[cfg(not(feature = "tz-system"))]
588 TzSystem => "tz-system",
589 #[cfg(not(feature = "tzdb-concatenated"))]
590 TzdbConcatenated => "tzdb-concatenated",
591 #[cfg(not(feature = "tzdb-zoneinfo"))]
592 TzdbZoneInfo => "tzdb-zoneinfo",
593 };
594 #[allow(unreachable_code)]
595 {
596 core::fmt::Display::fmt(name, f)?;
597 f.write_str("` is not enabled")
598 }
599 }
600}
601
602/// A `std::io::Error`.
603///
604/// This type is itself always available, even when the `std` feature is not
605/// enabled. When `std` is not enabled, a value of this type can never be
606/// constructed.
607///
608/// Otherwise, this type is a simple wrapper around `std::io::Error`. Its
609/// purpose is to encapsulate the conditional compilation based on the `std`
610/// feature.
611#[cfg_attr(not(feature = "alloc"), derive(Clone))]
612struct IOError {
613 #[cfg(feature = "std")]
614 err: std::io::Error,
615}
616
617#[cfg(feature = "std")]
618impl std::error::Error for IOError {}
619
620impl core::fmt::Display for IOError {
621 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
622 #[cfg(feature = "std")]
623 {
624 self.err.fmt(f)
625 }
626 #[cfg(not(feature = "std"))]
627 {
628 f.write_str("<BUG: SHOULD NOT EXIST>")
629 }
630 }
631}
632
633impl core::fmt::Debug for IOError {
634 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
635 #[cfg(feature = "std")]
636 {
637 f.debug_struct("IOError").field("err", &self.err).finish()
638 }
639 #[cfg(not(feature = "std"))]
640 {
641 f.write_str("<BUG: SHOULD NOT EXIST>")
642 }
643 }
644}
645
646#[cfg(feature = "std")]
647impl From<std::io::Error> for IOError {
648 fn from(err: std::io::Error) -> IOError {
649 IOError { err }
650 }
651}
652
653#[cfg_attr(not(feature = "alloc"), derive(Clone))]
654struct FilePathError {
655 #[cfg(feature = "std")]
656 path: std::path::PathBuf,
657}
658
659#[cfg(feature = "std")]
660impl std::error::Error for FilePathError {}
661
662impl core::fmt::Display for FilePathError {
663 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
664 #[cfg(feature = "std")]
665 {
666 self.path.display().fmt(f)
667 }
668 #[cfg(not(feature = "std"))]
669 {
670 f.write_str("<BUG: SHOULD NOT EXIST>")
671 }
672 }
673}
674
675impl core::fmt::Debug for FilePathError {
676 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
677 #[cfg(feature = "std")]
678 {
679 f.debug_struct("FilePathError").field("path", &self.path).finish()
680 }
681 #[cfg(not(feature = "std"))]
682 {
683 f.write_str("<BUG: SHOULD NOT EXIST>")
684 }
685 }
686}
687
688/// A simple trait to encapsulate automatic conversion to `Error`.
689///
690/// This trait basically exists to make `Error::context` work without needing
691/// to rely on public `From` impls. For example, without this trait, we might
692/// otherwise write `impl From<String> for Error`. But this would make it part
693/// of the public API. Which... maybe we should do, but at time of writing,
694/// I'm starting very conservative so that we can evolve errors in semver
695/// compatible ways.
696pub(crate) trait IntoError {
697 fn into_error(self) -> Error;
698}
699
700impl IntoError for Error {
701 #[inline(always)]
702 fn into_error(self) -> Error {
703 self
704 }
705}
706
707/// A trait for contextualizing error values.
708///
709/// This makes it easy to contextualize either `Error` or `Result<T, Error>`.
710/// Specifically, in the latter case, it absolves one of the need to call
711/// `map_err` everywhere one wants to add context to an error.
712///
713/// This trick was borrowed from `anyhow`.
714pub(crate) trait ErrorContext<T, E> {
715 /// Contextualize the given consequent error with this (`self`) error as
716 /// the cause.
717 ///
718 /// This is equivalent to saying that "consequent is caused by self."
719 ///
720 /// Note that if an `Error` is given for `kind`, then this panics if it has
721 /// a cause. (Because the cause would otherwise be dropped. An error causal
722 /// chain is just a linked list, not a tree.)
723 fn context(self, consequent: impl IntoError) -> Result<T, Error>;
724
725 /// Like `context`, but hides error construction within a closure.
726 ///
727 /// This is useful if the creation of the consequent error is not otherwise
728 /// guarded and when error construction is potentially "costly" (i.e., it
729 /// allocates). The closure avoids paying the cost of contextual error
730 /// creation in the happy path.
731 ///
732 /// Usually this only makes sense to use on a `Result<T, Error>`, otherwise
733 /// the closure is just executed immediately anyway.
734 fn with_context<C: IntoError>(
735 self,
736 consequent: impl FnOnce() -> C,
737 ) -> Result<T, Error>;
738}
739
740impl<T, E> ErrorContext<T, E> for Result<T, E>
741where
742 E: IntoError,
743{
744 #[cfg_attr(feature = "perf-inline", inline(always))]
745 fn context(self, consequent: impl IntoError) -> Result<T, Error> {
746 self.map_err(|err| {
747 err.into_error().context_impl(consequent.into_error())
748 })
749 }
750
751 #[cfg_attr(feature = "perf-inline", inline(always))]
752 fn with_context<C: IntoError>(
753 self,
754 consequent: impl FnOnce() -> C,
755 ) -> Result<T, Error> {
756 self.map_err(|err| {
757 err.into_error().context_impl(consequent().into_error())
758 })
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765
766 // We test that our 'Error' type is the size we expect. This isn't an API
767 // guarantee, but if the size increases, we really want to make sure we
768 // decide to do that intentionally. So this should be a speed bump. And in
769 // general, we should not increase the size without a very good reason.
770 #[test]
771 fn error_size() {
772 let mut expected_size = core::mem::size_of::<usize>();
773 if !cfg!(feature = "alloc") {
774 // oooowwwwwwwwwwwch.
775 //
776 // Like, this is horrible, right? core-only environments are
777 // precisely the place where one want to keep things slim. But
778 // in core-only, I don't know of a way to introduce any sort of
779 // indirection in the library level without using a completely
780 // different API.
781 //
782 // This is what makes me doubt that core-only Jiff is actually
783 // useful. In what context are people using a huge library like
784 // Jiff but can't define a small little heap allocator?
785 //
786 // OK, this used to be `expected_size *= 10`, but I slimmed it down
787 // to x3. Still kinda sucks right? If we tried harder, I think we
788 // could probably slim this down more. And if we were willing to
789 // sacrifice error message quality even more (like, all the way),
790 // then we could make `Error` a zero sized type. Which might
791 // actually be the right trade-off for core-only, but I'll hold off
792 // until we have some real world use cases.
793 //
794 // OK... after switching to structured errors, this jumped
795 // back up to `expected_size *= 6`. And that was with me being
796 // conscientious about what data we store inside of error types.
797 // Blech.
798 //
799 // 2026-01-14: A change to the `Offset` type made this move back
800 // down to `expected_size *= 4`.
801 expected_size *= 4;
802 }
803 assert_eq!(expected_size, core::mem::size_of::<Error>());
804 }
805}