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(_) | JcoreRange(_)
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 /// Builds a `jiff::Error` from a `jcore::tz::posix::ParseError`.
208 ///
209 /// This very much cannot be added as a `From` impl because that would
210 /// introduce a public dependency on `jcore`.
211 #[inline(never)]
212 #[cold]
213 pub(crate) fn jcore_posix_parse(
214 err: jcore::tz::posix::ParseError,
215 ) -> Error {
216 Error::from(ErrorKind::JcorePosixParse(err))
217 }
218
219 /// Builds a `jiff::Error` from a `jcore::tz::tzif::ParseError`.
220 ///
221 /// This very much cannot be added as a `From` impl because that would
222 /// introduce a public dependency on `jcore`.
223 #[cfg(feature = "alloc")]
224 #[inline(never)]
225 #[cold]
226 pub(crate) fn jcore_tzif_parse(err: jcore::tz::tzif::ParseError) -> Error {
227 Error::from(ErrorKind::JcoreTzifParse(err))
228 }
229
230 /// Builds a `jiff::Error` from a `jcore::bounds::RangeError`.
231 ///
232 /// This very much cannot be added as a `From` impl because that would
233 /// introduce a public dependency on `jcore`.
234 #[inline(never)]
235 #[cold]
236 pub(crate) fn jcore_range(err: jcore::bounds::RangeError) -> Error {
237 Error::from(ErrorKind::JcoreRange(err))
238 }
239
240 #[inline(never)]
241 #[cold]
242 pub(crate) fn special_bounds(err: SpecialBoundsError) -> Error {
243 Error::from(ErrorKind::SpecialBounds(err))
244 }
245
246 /// A convenience constructor for building an I/O error.
247 ///
248 /// This returns an error that is just a simple wrapper around the
249 /// `std::io::Error` type. In general, callers should always attach some
250 /// kind of context to this error (like a file path).
251 ///
252 /// This is only available when the `std` feature is enabled.
253 #[cfg(feature = "std")]
254 #[inline(never)]
255 #[cold]
256 pub(crate) fn io(err: std::io::Error) -> Error {
257 Error::from(ErrorKind::IO(IOError { err }))
258 }
259
260 /// Contextualizes this error by associating the given file path with it.
261 ///
262 /// This is a convenience routine for calling `Error::context` with a
263 /// `FilePathError`.
264 #[cfg(any(feature = "tzdb-zoneinfo", feature = "tzdb-concatenated"))]
265 #[inline(never)]
266 #[cold]
267 pub(crate) fn path(self, path: impl Into<std::path::PathBuf>) -> Error {
268 let err = Error::from(ErrorKind::FilePath(FilePathError {
269 path: path.into(),
270 }));
271 self.context(err)
272 }
273
274 /*
275 /// Creates a new "unknown" Jiff error.
276 ///
277 /// The benefit of this API is that it permits creating an `Error` in a
278 /// `const` context. But the error message quality is currently pretty
279 /// bad: it's just a generic "unknown Jiff error" message.
280 ///
281 /// This could be improved to take a `&'static str`, but I believe this
282 /// will require pointer tagging in order to avoid increasing the size of
283 /// `Error`. (Which is important, because of how many perf sensitive
284 /// APIs return a `Result<T, Error>` in Jiff.
285 pub(crate) const fn unknown() -> Error {
286 Error { inner: None }
287 }
288 */
289
290 #[cfg_attr(feature = "perf-inline", inline(always))]
291 pub(crate) fn context(self, consequent: impl IntoError) -> Error {
292 self.context_impl(consequent.into_error())
293 }
294
295 #[inline(never)]
296 #[cold]
297 fn context_impl(self, _consequent: Error) -> Error {
298 #[cfg(feature = "alloc")]
299 {
300 let mut err = _consequent;
301 if err.inner.is_none() {
302 err = Error::from(ErrorKind::Unknown);
303 }
304 let inner = err.inner.as_mut().unwrap();
305 assert!(
306 inner.cause.is_none(),
307 "cause of consequence must be `None`"
308 );
309 // OK because we just created this error so the Arc
310 // has one reference.
311 Arc::get_mut(inner).unwrap().cause = Some(self);
312 err
313 }
314 #[cfg(not(feature = "alloc"))]
315 {
316 // We just completely drop `self`. :-(
317 //
318 // 2025-12-21: ... actually, we used to drop self, but this
319 // ends up dropping the root cause. And the root cause
320 // is how the predicates on `Error` work. So we drop the
321 // consequent instead.
322 self
323 }
324 }
325
326 /// Returns the root error in this chain.
327 fn root(&self) -> &Error {
328 // OK because `Error::chain` is guaranteed to return a non-empty
329 // iterator.
330 self.chain().last().unwrap()
331 }
332
333 /// Returns a chain of error values.
334 ///
335 /// This starts with the most recent error added to the chain. That is,
336 /// the highest level context. The last error in the chain is always the
337 /// "root" cause. That is, the error closest to the point where something
338 /// has gone wrong.
339 ///
340 /// The iterator returned is guaranteed to yield at least one error.
341 fn chain(&self) -> impl Iterator<Item = &Error> {
342 #[cfg(feature = "alloc")]
343 {
344 let mut err = self;
345 core::iter::once(err).chain(core::iter::from_fn(move || {
346 err = err
347 .inner
348 .as_ref()
349 .and_then(|inner| inner.cause.as_ref())?;
350 Some(err)
351 }))
352 }
353 #[cfg(not(feature = "alloc"))]
354 {
355 core::iter::once(self)
356 }
357 }
358
359 /// Returns the kind of this error.
360 fn kind(&self) -> &ErrorKind {
361 self.inner
362 .as_ref()
363 .map(|inner| &inner.kind)
364 .unwrap_or(&ErrorKind::Unknown)
365 }
366}
367
368#[cfg(feature = "std")]
369impl std::error::Error for Error {}
370
371impl core::fmt::Display for Error {
372 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
373 let mut it = self.chain().peekable();
374 while let Some(err) = it.next() {
375 core::fmt::Display::fmt(err.kind(), f)?;
376 if it.peek().is_some() {
377 f.write_str(": ")?;
378 }
379 }
380 Ok(())
381 }
382}
383
384impl core::fmt::Debug for Error {
385 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
386 if !f.alternate() {
387 core::fmt::Display::fmt(self, f)
388 } else {
389 let Some(ref inner) = self.inner else {
390 return f
391 .debug_struct("Error")
392 .field("kind", &"None")
393 .finish();
394 };
395 #[cfg(feature = "alloc")]
396 {
397 f.debug_struct("Error")
398 .field("kind", &inner.kind)
399 .field("cause", &inner.cause)
400 .finish()
401 }
402 #[cfg(not(feature = "alloc"))]
403 {
404 f.debug_struct("Error").field("kind", &inner.kind).finish()
405 }
406 }
407 }
408}
409
410#[cfg(feature = "defmt")]
411impl defmt::Format for Error {
412 fn format(&self, f: defmt::Formatter) {
413 let Some(ref inner) = self.inner else {
414 return defmt::write!(f, "Error {{ kind: None }}");
415 };
416 #[cfg(feature = "alloc")]
417 {
418 defmt::write!(
419 f,
420 "Error {{ kind: {}, cause: {} }}",
421 inner.kind,
422 inner.cause
423 );
424 }
425 #[cfg(not(feature = "alloc"))]
426 {
427 defmt::write!(f, "Error {{ kind: {} }}", inner.kind);
428 }
429 }
430}
431
432/// The underlying kind of a [`Error`].
433#[derive(Debug)]
434#[cfg_attr(not(feature = "alloc"), derive(Clone))]
435#[cfg_attr(feature = "defmt", derive(defmt::Format))]
436enum ErrorKind {
437 Adhoc(AdhocError),
438 Bounds(BoundsError),
439 Civil(self::civil::Error),
440 CrateFeature(CrateFeatureError),
441 Duration(self::duration::Error),
442 #[allow(dead_code)] // not used in some feature configs
443 FilePath(FilePathError),
444 Fmt(self::fmt::Error),
445 FmtFriendly(self::fmt::friendly::Error),
446 FmtOffset(self::fmt::offset::Error),
447 FmtRfc2822(self::fmt::rfc2822::Error),
448 FmtRfc9557(self::fmt::rfc9557::Error),
449 FmtTemporal(self::fmt::temporal::Error),
450 FmtUtil(self::fmt::util::Error),
451 FmtStrtime(self::fmt::strtime::Error),
452 FmtStrtimeFormat(self::fmt::strtime::FormatError),
453 FmtStrtimeParse(self::fmt::strtime::ParseError),
454 #[allow(dead_code)] // not used in some feature configs
455 IO(IOError),
456 JcorePosixParse(jcore::tz::posix::ParseError),
457 #[cfg(feature = "alloc")]
458 JcoreTzifParse(jcore::tz::tzif::ParseError),
459 JcoreRange(jcore::bounds::RangeError),
460 OsStrUtf8(self::util::OsStrUtf8Error),
461 ParseInt(self::util::ParseIntError),
462 ParseFraction(self::util::ParseFractionError),
463 RoundingIncrement(self::util::RoundingIncrementError),
464 SignedDuration(self::signed_duration::Error),
465 Span(self::span::Error),
466 UnitConfig(self::unit::UnitConfigError),
467 SpecialBounds(SpecialBoundsError),
468 Timestamp(self::timestamp::Error),
469 TzAmbiguous(self::tz::ambiguous::Error),
470 TzDb(self::tz::db::Error),
471 TzConcatenated(self::tz::concatenated::Error),
472 TzOffset(self::tz::offset::Error),
473 TzSystem(self::tz::system::Error),
474 TzTimeZone(self::tz::timezone::Error),
475 #[allow(dead_code)]
476 TzZic(self::tz::zic::Error),
477 Unknown,
478 Zoned(self::zoned::Error),
479}
480
481impl core::fmt::Display for ErrorKind {
482 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
483 use self::ErrorKind::*;
484
485 match *self {
486 Adhoc(ref msg) => msg.fmt(f),
487 Bounds(ref msg) => msg.fmt(f),
488 JcorePosixParse(ref msg) => msg.fmt(f),
489 #[cfg(feature = "alloc")]
490 JcoreTzifParse(ref msg) => msg.fmt(f),
491 JcoreRange(ref msg) => msg.fmt(f),
492 Civil(ref err) => err.fmt(f),
493 CrateFeature(ref err) => err.fmt(f),
494 Duration(ref err) => err.fmt(f),
495 FilePath(ref err) => err.fmt(f),
496 Fmt(ref err) => err.fmt(f),
497 FmtFriendly(ref err) => err.fmt(f),
498 FmtOffset(ref err) => err.fmt(f),
499 FmtRfc2822(ref err) => err.fmt(f),
500 FmtRfc9557(ref err) => err.fmt(f),
501 FmtUtil(ref err) => err.fmt(f),
502 FmtStrtime(ref err) => err.fmt(f),
503 FmtStrtimeFormat(ref err) => err.fmt(f),
504 FmtStrtimeParse(ref err) => err.fmt(f),
505 FmtTemporal(ref err) => err.fmt(f),
506 IO(ref err) => err.fmt(f),
507 OsStrUtf8(ref err) => err.fmt(f),
508 ParseInt(ref err) => err.fmt(f),
509 ParseFraction(ref err) => err.fmt(f),
510 RoundingIncrement(ref err) => err.fmt(f),
511 SignedDuration(ref err) => err.fmt(f),
512 Span(ref err) => err.fmt(f),
513 UnitConfig(ref err) => err.fmt(f),
514 SpecialBounds(ref msg) => msg.fmt(f),
515 Timestamp(ref err) => err.fmt(f),
516 TzAmbiguous(ref err) => err.fmt(f),
517 TzDb(ref err) => err.fmt(f),
518 TzConcatenated(ref err) => err.fmt(f),
519 TzOffset(ref err) => err.fmt(f),
520 TzSystem(ref err) => err.fmt(f),
521 TzTimeZone(ref err) => err.fmt(f),
522 TzZic(ref err) => err.fmt(f),
523 Unknown => f.write_str("unknown Jiff error"),
524 Zoned(ref err) => err.fmt(f),
525 }
526 }
527}
528
529impl From<ErrorKind> for Error {
530 fn from(kind: ErrorKind) -> Error {
531 #[cfg(feature = "alloc")]
532 {
533 Error { inner: Some(Arc::new(ErrorInner { kind, cause: None })) }
534 }
535 #[cfg(not(feature = "alloc"))]
536 {
537 Error { inner: Some(Arc::new(ErrorInner { kind })) }
538 }
539 }
540}
541
542/// A generic error message.
543///
544/// This used to be used to represent most errors in Jiff. But then I switched
545/// to more structured error types (internally). We still keep this around to
546/// support the `Error::from_args` public API, which permits users of Jiff to
547/// manifest their own `Error` values from an arbitrary message.
548#[cfg_attr(not(feature = "alloc"), derive(Clone))]
549#[cfg_attr(feature = "defmt", derive(defmt::Format))]
550struct AdhocError {
551 #[cfg(feature = "alloc")]
552 message: alloc::boxed::Box<str>,
553 #[cfg(not(feature = "alloc"))]
554 message: &'static str,
555}
556
557impl AdhocError {
558 fn from_args<'a>(message: core::fmt::Arguments<'a>) -> AdhocError {
559 #[cfg(feature = "alloc")]
560 {
561 use alloc::string::ToString;
562
563 let message = message.to_string().into_boxed_str();
564 AdhocError { message }
565 }
566 #[cfg(not(feature = "alloc"))]
567 {
568 let message = message.as_str().unwrap_or(
569 "unknown Jiff error (better error messages require \
570 enabling the `alloc` feature for the `jiff` crate)",
571 );
572 AdhocError { message }
573 }
574 }
575}
576
577#[cfg(feature = "std")]
578impl std::error::Error for AdhocError {}
579
580impl core::fmt::Display for AdhocError {
581 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
582 core::fmt::Display::fmt(&self.message, f)
583 }
584}
585
586impl core::fmt::Debug for AdhocError {
587 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
588 core::fmt::Debug::fmt(&self.message, f)
589 }
590}
591
592/// An error used whenever a failure is caused by a missing crate feature.
593///
594/// This enum doesn't necessarily contain every Jiff crate feature. It only
595/// contains the features whose absence can result in an error.
596#[derive(Clone, Debug)]
597#[cfg_attr(feature = "defmt", derive(defmt::Format))]
598pub(crate) enum CrateFeatureError {
599 #[cfg(not(feature = "tz-system"))]
600 TzSystem,
601 #[cfg(not(feature = "tzdb-concatenated"))]
602 TzdbConcatenated,
603 #[cfg(not(feature = "tzdb-zoneinfo"))]
604 TzdbZoneInfo,
605}
606
607impl From<CrateFeatureError> for Error {
608 #[cold]
609 #[inline(never)]
610 fn from(err: CrateFeatureError) -> Error {
611 ErrorKind::CrateFeature(err).into()
612 }
613}
614
615impl core::fmt::Display for CrateFeatureError {
616 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
617 #[allow(unused_imports)]
618 use self::CrateFeatureError::*;
619
620 f.write_str("operation failed because Jiff crate feature `")?;
621 #[allow(unused_variables)]
622 let name: &str = match *self {
623 #[cfg(not(feature = "tz-system"))]
624 TzSystem => "tz-system",
625 #[cfg(not(feature = "tzdb-concatenated"))]
626 TzdbConcatenated => "tzdb-concatenated",
627 #[cfg(not(feature = "tzdb-zoneinfo"))]
628 TzdbZoneInfo => "tzdb-zoneinfo",
629 };
630 #[allow(unreachable_code)]
631 {
632 core::fmt::Display::fmt(name, f)?;
633 f.write_str("` is not enabled")
634 }
635 }
636}
637
638/// A `std::io::Error`.
639///
640/// This type is itself always available, even when the `std` feature is not
641/// enabled. When `std` is not enabled, a value of this type can never be
642/// constructed.
643///
644/// Otherwise, this type is a simple wrapper around `std::io::Error`. Its
645/// purpose is to encapsulate the conditional compilation based on the `std`
646/// feature.
647#[cfg_attr(not(feature = "alloc"), derive(Clone))]
648struct IOError {
649 #[cfg(feature = "std")]
650 err: std::io::Error,
651}
652
653#[cfg(feature = "std")]
654impl std::error::Error for IOError {}
655
656impl core::fmt::Display for IOError {
657 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
658 #[cfg(feature = "std")]
659 {
660 self.err.fmt(f)
661 }
662 #[cfg(not(feature = "std"))]
663 {
664 f.write_str("<BUG: SHOULD NOT EXIST>")
665 }
666 }
667}
668
669impl core::fmt::Debug for IOError {
670 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
671 #[cfg(feature = "std")]
672 {
673 f.debug_struct("IOError").field("err", &self.err).finish()
674 }
675 #[cfg(not(feature = "std"))]
676 {
677 f.write_str("<BUG: SHOULD NOT EXIST>")
678 }
679 }
680}
681
682#[cfg(feature = "defmt")]
683impl defmt::Format for IOError {
684 fn format(&self, f: defmt::Formatter) {
685 // `std::io::Error` does not implement `defmt::Format`. Since this
686 // error is std-only and defmt is mainly used in embedded contexts,
687 // omitting the error is probably fine.
688 defmt::write!(f, "IOError(unavailable)");
689 }
690}
691
692#[cfg(feature = "std")]
693impl From<std::io::Error> for IOError {
694 fn from(err: std::io::Error) -> IOError {
695 IOError { err }
696 }
697}
698
699#[cfg_attr(not(feature = "alloc"), derive(Clone))]
700struct FilePathError {
701 #[cfg(feature = "std")]
702 path: std::path::PathBuf,
703}
704
705#[cfg(feature = "std")]
706impl std::error::Error for FilePathError {}
707
708impl core::fmt::Display for FilePathError {
709 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
710 #[cfg(feature = "std")]
711 {
712 self.path.display().fmt(f)
713 }
714 #[cfg(not(feature = "std"))]
715 {
716 f.write_str("<BUG: SHOULD NOT EXIST>")
717 }
718 }
719}
720
721impl core::fmt::Debug for FilePathError {
722 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
723 #[cfg(feature = "std")]
724 {
725 f.debug_struct("FilePathError").field("path", &self.path).finish()
726 }
727 #[cfg(not(feature = "std"))]
728 {
729 f.write_str("<BUG: SHOULD NOT EXIST>")
730 }
731 }
732}
733
734#[cfg(feature = "defmt")]
735impl defmt::Format for FilePathError {
736 fn format(&self, f: defmt::Formatter) {
737 // `std::path::PathBuf` does not implement `defmt::Format`. Since this
738 // error is std-only and defmt is mainly used in embedded contexts,
739 // omitting the path is probably fine.
740 defmt::write!(f, "FilePathError(unavailable)");
741 }
742}
743
744/// A simple trait to encapsulate automatic conversion to `Error`.
745///
746/// This trait basically exists to make `Error::context` work without needing
747/// to rely on public `From` impls. For example, without this trait, we might
748/// otherwise write `impl From<String> for Error`. But this would make it part
749/// of the public API. Which... maybe we should do, but at time of writing,
750/// I'm starting very conservative so that we can evolve errors in semver
751/// compatible ways.
752pub(crate) trait IntoError {
753 fn into_error(self) -> Error;
754}
755
756impl IntoError for Error {
757 #[inline(always)]
758 fn into_error(self) -> Error {
759 self
760 }
761}
762
763// This trait impl is okay because `IntoError` is a crate-internal trait. So
764// this impl will not cause a public dependency on `jcore`.
765impl IntoError for jcore::bounds::RangeError {
766 #[inline(always)]
767 fn into_error(self) -> Error {
768 Error::jcore_range(self)
769 }
770}
771
772/// A trait for contextualizing error values.
773///
774/// This makes it easy to contextualize either `Error` or `Result<T, Error>`.
775/// Specifically, in the latter case, it absolves one of the need to call
776/// `map_err` everywhere one wants to add context to an error.
777///
778/// This trick was borrowed from `anyhow`.
779pub(crate) trait ErrorContext<T, E> {
780 /// Contextualize the given consequent error with this (`self`) error as
781 /// the cause.
782 ///
783 /// This is equivalent to saying that "consequent is caused by self."
784 ///
785 /// Note that if an `Error` is given for `kind`, then this panics if it has
786 /// a cause. (Because the cause would otherwise be dropped. An error causal
787 /// chain is just a linked list, not a tree.)
788 fn context(self, consequent: impl IntoError) -> Result<T, Error>;
789
790 /// Like `context`, but hides error construction within a closure.
791 ///
792 /// This is useful if the creation of the consequent error is not otherwise
793 /// guarded and when error construction is potentially "costly" (i.e., it
794 /// allocates). The closure avoids paying the cost of contextual error
795 /// creation in the happy path.
796 ///
797 /// Usually this only makes sense to use on a `Result<T, Error>`, otherwise
798 /// the closure is just executed immediately anyway.
799 fn with_context<C: IntoError>(
800 self,
801 consequent: impl FnOnce() -> C,
802 ) -> Result<T, Error>;
803}
804
805impl<T, E> ErrorContext<T, E> for Result<T, E>
806where
807 E: IntoError,
808{
809 #[cfg_attr(feature = "perf-inline", inline(always))]
810 fn context(self, consequent: impl IntoError) -> Result<T, Error> {
811 self.map_err(|err| {
812 err.into_error().context_impl(consequent.into_error())
813 })
814 }
815
816 #[cfg_attr(feature = "perf-inline", inline(always))]
817 fn with_context<C: IntoError>(
818 self,
819 consequent: impl FnOnce() -> C,
820 ) -> Result<T, Error> {
821 self.map_err(|err| {
822 err.into_error().context_impl(consequent().into_error())
823 })
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830
831 // We test that our 'Error' type is the size we expect. This isn't an API
832 // guarantee, but if the size increases, we really want to make sure we
833 // decide to do that intentionally. So this should be a speed bump. And in
834 // general, we should not increase the size without a very good reason.
835 #[test]
836 fn error_size() {
837 if cfg!(feature = "alloc") {
838 let expected_size = core::mem::size_of::<usize>();
839 assert_eq!(expected_size, core::mem::size_of::<Error>());
840 return;
841 }
842 // oooowwwwwwwwwwwch.
843 //
844 // Like, this is horrible, right? core-only environments are
845 // precisely the place where one want to keep things slim. But
846 // in core-only, I don't know of a way to introduce any sort of
847 // indirection in the library level without using a completely
848 // different API.
849 //
850 // This is what makes me doubt that core-only Jiff is actually
851 // useful. In what context are people using a huge library like
852 // Jiff but can't define a small little heap allocator?
853 //
854 // OK, this used to be `expected_size *= 10`, but I slimmed it down
855 // to x3. Still kinda sucks right? If we tried harder, I think we
856 // could probably slim this down more. And if we were willing to
857 // sacrifice error message quality even more (like, all the way),
858 // then we could make `Error` a zero sized type. Which might
859 // actually be the right trade-off for core-only, but I'll hold off
860 // until we have some real world use cases.
861 //
862 // OK... after switching to structured errors, this jumped
863 // back up to `expected_size *= 6`. And that was with me being
864 // conscientious about what data we store inside of error types.
865 // Blech.
866 //
867 // 2026-01-14: A change to the `Offset` type made this move back
868 // down to `expected_size *= 4`.
869 //
870 // 2026-05-28: No changes here, but `4 * pointer-size` is not the
871 // right calculation here. This is unfortunately coupled with an
872 // internal representation for the biggest possible error variant.
873 // And also compiler optimizations. But at time of writing, it's
874 // from the `jiff::error::tz::offset::Error` enum. We get 4 32-bit
875 // integers (always 32-bit) plus one pointer sized discriminant.
876 //
877 // 2026-05-28 redux: this now seems coupled with compiler
878 // optimizations. So just give up and check that it's reasonable.
879 let got = core::mem::size_of::<Error>();
880 assert!(got <= 40, "wanted error size to be <= 40, but got {got}");
881 }
882}