Skip to main content

vortex_error/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4#![deny(missing_docs)]
5
6//! This crate defines error & result types for Vortex.
7//! It also contains a variety of useful macros for error handling.
8
9use std::backtrace::Backtrace;
10use std::backtrace::BacktraceStatus;
11use std::borrow::Cow;
12use std::convert::Infallible;
13use std::env;
14use std::error::Error;
15use std::fmt;
16use std::fmt::Debug;
17use std::fmt::Display;
18use std::fmt::Formatter;
19use std::io;
20use std::num::TryFromIntError;
21use std::ops::Deref;
22use std::sync::Arc;
23use std::sync::LazyLock;
24
25/// A string that can be used as an error message.
26#[derive(Debug)]
27pub struct ErrString(Cow<'static, str>);
28
29#[expect(
30    clippy::fallible_impl_from,
31    reason = "intentionally panic in debug mode when VORTEX_PANIC_ON_ERR is set"
32)]
33impl<T> From<T> for ErrString
34where
35    T: Into<Cow<'static, str>>,
36{
37    #[expect(
38        clippy::panic,
39        reason = "intentionally panic in debug mode when VORTEX_PANIC_ON_ERR is set"
40    )]
41    fn from(msg: T) -> Self {
42        if panic_on_err() {
43            panic!("{}\nBacktrace:\n{}", msg.into(), Backtrace::capture());
44        } else {
45            Self(msg.into())
46        }
47    }
48}
49
50fn panic_on_err() -> bool {
51    static PANIC_ON_ERR: LazyLock<bool> =
52        LazyLock::new(|| env::var("VORTEX_PANIC_ON_ERR").is_ok_and(|v| v == "1"));
53    *PANIC_ON_ERR
54}
55
56impl AsRef<str> for ErrString {
57    fn as_ref(&self) -> &str {
58        &self.0
59    }
60}
61
62impl Deref for ErrString {
63    type Target = str;
64
65    fn deref(&self) -> &Self::Target {
66        &self.0
67    }
68}
69
70impl Display for ErrString {
71    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
72        Display::fmt(&self.0, f)
73    }
74}
75
76impl From<Infallible> for VortexError {
77    fn from(_: Infallible) -> Self {
78        unreachable!()
79    }
80}
81
82const _: () = assert!(size_of::<VortexError>() < 128);
83
84/// The top-level error type for Vortex.
85#[non_exhaustive]
86pub enum VortexError {
87    /// A catch-all error variant
88    Other(ErrString, Box<Backtrace>),
89    /// A wrapped external error
90    External(Box<dyn Error + Send + Sync + 'static>, Box<Backtrace>),
91    /// An index is out of bounds.
92    OutOfBounds(usize, usize, usize, Box<Backtrace>),
93    /// An error occurred while executing a compute kernel.
94    Compute(ErrString, Box<Backtrace>),
95    /// An invalid argument was provided.
96    InvalidArgument(ErrString, Box<Backtrace>),
97    /// An error occurred while serializing or deserializing.
98    Serde(ErrString, Box<Backtrace>),
99    /// An unimplemented function was called.
100    NotImplemented(ErrString, ErrString, Box<Backtrace>),
101    /// A type mismatch occurred.
102    MismatchedTypes(ErrString, ErrString, Box<Backtrace>),
103    /// An assertion failed.
104    AssertionFailed(ErrString, Box<Backtrace>),
105    /// A wrapper for other errors, carrying additional context.
106    Context(ErrString, Box<VortexError>),
107    /// A wrapper for shared errors that require cloning.
108    Shared(Arc<VortexError>),
109    /// A wrapper for errors from the Arrow library.
110    Arrow(arrow_schema::ArrowError, Box<Backtrace>),
111    /// A wrapper for errors from the FlatBuffers library.
112    #[cfg(feature = "flatbuffers")]
113    FlatBuffers(flatbuffers::InvalidFlatbuffer, Box<Backtrace>),
114    /// A wrapper for formatting errors.
115    Fmt(fmt::Error, Box<Backtrace>),
116    /// A wrapper for IO errors.
117    Io(io::Error, Box<Backtrace>),
118    /// A wrapper for errors from the Object Store library.
119    #[cfg(feature = "object_store")]
120    ObjectStore(object_store::Error, Box<Backtrace>),
121    /// A wrapper for errors from the Jiff library.
122    Jiff(jiff::Error, Box<Backtrace>),
123    /// A wrapper for Tokio join error.
124    #[cfg(feature = "tokio")]
125    Join(tokio::task::JoinError, Box<Backtrace>),
126    /// Wrap errors for fallible integer casting.
127    TryFromInt(TryFromIntError, Box<Backtrace>),
128    /// Wrap protobuf-related errors
129    Prost(Box<dyn Error + Send + Sync + 'static>, Box<Backtrace>),
130}
131
132impl VortexError {
133    /// Adds additional context to an error.
134    pub fn with_context<T: Into<ErrString>>(self, msg: T) -> Self {
135        VortexError::Context(msg.into(), Box::new(self))
136    }
137
138    /// Error prefix by variant
139    fn variant_prefix(&self) -> &'static str {
140        use VortexError::*;
141
142        match self {
143            Other(..) => "Other error: ",
144            External(..) => "External error: ",
145            OutOfBounds(..) => "Out of bounds error: ",
146            Compute(..) => "Compute error: ",
147            InvalidArgument(..) => "Invalid argument error: ",
148            Serde(..) => "Serde error: ",
149            NotImplemented(..) => "Not implemented error: ",
150            MismatchedTypes(..) => "Mismatched types error: ",
151            AssertionFailed(..) => "Assertion failed error: ",
152            Context(..) | Shared(..) => "", // basically delegate to the underlying one
153            Arrow(..) => "Arrow error: ",
154            #[cfg(feature = "flatbuffers")]
155            FlatBuffers(..) => "Flat buffers error: ",
156            Fmt(..) => "Fmt: ",
157            Io(..) => "Io: ",
158            #[cfg(feature = "object_store")]
159            ObjectStore(..) => "Object store error: ",
160            Jiff(..) => "Jiff error: ",
161            #[cfg(feature = "tokio")]
162            Join(..) => "Tokio join error:",
163            TryFromInt(..) => "Try from int error:",
164            Prost(..) => "Prost error:",
165        }
166    }
167
168    fn backtrace(&self) -> Option<&Backtrace> {
169        use VortexError::*;
170
171        match self {
172            Other(.., bt) => Some(bt.as_ref()),
173            External(.., bt) => Some(bt.as_ref()),
174            OutOfBounds(.., bt) => Some(bt.as_ref()),
175            Compute(.., bt) => Some(bt.as_ref()),
176            InvalidArgument(.., bt) => Some(bt.as_ref()),
177            Serde(.., bt) => Some(bt.as_ref()),
178            NotImplemented(.., bt) => Some(bt.as_ref()),
179            MismatchedTypes(.., bt) => Some(bt.as_ref()),
180            AssertionFailed(.., bt) => Some(bt.as_ref()),
181            Arrow(.., bt) => Some(bt.as_ref()),
182            #[cfg(feature = "flatbuffers")]
183            FlatBuffers(.., bt) => Some(bt.as_ref()),
184            Fmt(.., bt) => Some(bt.as_ref()),
185            Io(.., bt) => Some(bt.as_ref()),
186            #[cfg(feature = "object_store")]
187            ObjectStore(.., bt) => Some(bt.as_ref()),
188            Jiff(.., bt) => Some(bt.as_ref()),
189            #[cfg(feature = "tokio")]
190            Join(.., bt) => Some(bt.as_ref()),
191            TryFromInt(.., bt) => Some(bt.as_ref()),
192            Prost(.., bt) => Some(bt.as_ref()),
193            Context(_, inner) => inner.backtrace(),
194            Shared(inner) => inner.backtrace(),
195        }
196    }
197
198    fn message(&self) -> String {
199        use VortexError::*;
200
201        match self {
202            Other(msg, _) => msg.to_string(),
203            External(err, _) => err.to_string(),
204            OutOfBounds(idx, start, stop, _) => {
205                format!("index {idx} out of bounds from {start} to {stop}")
206            }
207            Compute(msg, _) | InvalidArgument(msg, _) | Serde(msg, _) | AssertionFailed(msg, _) => {
208                format!("{msg}")
209            }
210            NotImplemented(func, by_whom, _) => {
211                format!("function {func} not implemented for {by_whom}")
212            }
213            MismatchedTypes(expected, actual, _) => {
214                format!("expected type: {expected} but instead got {actual}")
215            }
216            Context(msg, inner) => {
217                format!("{msg}:\n  {inner}")
218            }
219            Shared(inner) => inner.message(),
220            Arrow(err, _) => {
221                format!("{err}")
222            }
223            #[cfg(feature = "flatbuffers")]
224            FlatBuffers(err, _) => {
225                format!("{err}")
226            }
227            Fmt(err, _) => {
228                format!("{err}")
229            }
230            Io(err, _) => {
231                format!("{err}")
232            }
233            #[cfg(feature = "object_store")]
234            ObjectStore(err, _) => {
235                format!("{err}")
236            }
237            Jiff(err, _) => {
238                format!("{err}")
239            }
240            #[cfg(feature = "tokio")]
241            Join(err, _) => {
242                format!("{err}")
243            }
244            TryFromInt(err, _) => {
245                format!("{err}")
246            }
247            Prost(err, _) => {
248                format!("{err}")
249            }
250        }
251    }
252}
253
254impl Display for VortexError {
255    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
256        write!(f, "{}", self.variant_prefix())?;
257        write!(f, "{}", self.message())?;
258        if let Some(backtrace) = self.backtrace()
259            && backtrace.status() == BacktraceStatus::Captured
260        {
261            write!(f, "\nBacktrace:\n{backtrace}")?;
262        }
263
264        Ok(())
265    }
266}
267
268impl Debug for VortexError {
269    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
270        write!(f, "{self}")
271    }
272}
273
274impl Error for VortexError {
275    fn source(&self) -> Option<&(dyn Error + 'static)> {
276        use VortexError::*;
277
278        match self {
279            External(err, _) => Some(err.as_ref()),
280            Context(_, inner) => inner.source(),
281            Shared(inner) => inner.source(),
282            Arrow(err, _) => Some(err),
283            #[cfg(feature = "flatbuffers")]
284            FlatBuffers(err, _) => Some(err),
285            Io(err, _) => Some(err),
286            #[cfg(feature = "object_store")]
287            ObjectStore(err, _) => Some(err),
288            Jiff(err, _) => Some(err),
289            #[cfg(feature = "tokio")]
290            Join(err, _) => Some(err),
291            Prost(err, _) => Some(err.as_ref()),
292            _ => None,
293        }
294    }
295}
296
297/// A type alias for Results that return VortexErrors as their error type.
298pub type VortexResult<T> = Result<T, VortexError>;
299
300/// A vortex result that can be shared or cloned.
301pub type SharedVortexResult<T> = Result<T, Arc<VortexError>>;
302
303impl From<Arc<VortexError>> for VortexError {
304    fn from(value: Arc<VortexError>) -> Self {
305        Self::from(&value)
306    }
307}
308
309impl From<&Arc<VortexError>> for VortexError {
310    fn from(e: &Arc<VortexError>) -> Self {
311        if let VortexError::Shared(e_inner) = e.as_ref() {
312            // don't re-wrap
313            VortexError::Shared(Arc::clone(e_inner))
314        } else {
315            VortexError::Shared(Arc::clone(e))
316        }
317    }
318}
319
320/// A trait for expect-ing a VortexResult or an Option.
321pub trait VortexExpect {
322    /// The type of the value being expected.
323    type Output;
324
325    /// Returns the value of the result if it is Ok, otherwise panics with the error.
326    /// Should be called only in contexts where the error condition represents a bug (programmer error).
327    ///
328    /// # `&'static` message lifetime
329    ///
330    /// The panic string argument should be a string literal, hence the `&'static` lifetime. If
331    /// you'd like to panic with a dynamic format string, consider using `unwrap_or_else` combined
332    /// with the `vortex_panic!` macro instead.
333    fn vortex_expect(self, msg: &'static str) -> Self::Output;
334}
335
336impl<T, E> VortexExpect for Result<T, E>
337where
338    E: Into<VortexError>,
339{
340    type Output = T;
341
342    #[allow(clippy::inline_always)]
343    #[inline(always)]
344    fn vortex_expect(self, msg: &'static str) -> Self::Output {
345        match self {
346            Ok(value) => value,
347            Err(err) => __private::result_expect_failed(err, msg),
348        }
349    }
350}
351
352impl<T> VortexExpect for Option<T> {
353    type Output = T;
354
355    #[allow(clippy::inline_always)]
356    #[inline(always)]
357    fn vortex_expect(self, msg: &'static str) -> Self::Output {
358        match self {
359            Some(value) => value,
360            None => __private::option_expect_failed(msg),
361        }
362    }
363}
364
365/// A convenient macro for creating a VortexError.
366#[macro_export]
367macro_rules! vortex_err {
368    (Other: $($tts:tt)*) => {
369        $crate::__private::fmt_err($crate::VortexError::Other, &format_args!($($tts)*))
370    };
371    (AssertionFailed: $($tts:tt)*) => {
372        $crate::__private::fmt_err($crate::VortexError::AssertionFailed, &format_args!($($tts)*))
373    };
374    (IOError: $($tts:tt)*) => {{
375        use std::backtrace::Backtrace;
376        $crate::__private::must_use(
377            $crate::VortexError::IOError(err_string.into(), Box::new(Backtrace::capture()))
378        )
379    }};
380    (OutOfBounds: $idx:expr, $start:expr, $stop:expr) => {
381        $crate::__private::out_of_bounds($idx, $start, $stop)
382    };
383    (NotImplemented: $func:expr, $by_whom:expr) => {
384        $crate::__private::not_implemented($func, &format_args!("{}", $by_whom))
385    };
386    (MismatchedTypes: $expected:expr, $actual:expr) => {
387        $crate::__private::mismatched_types(&$expected, &$actual)
388    };
389    (Context: $msg:literal, $err:expr) => {
390        $crate::__private::context($msg, $err)
391    };
392    (External: $err:expr) => {
393        $crate::__private::external($err)
394    };
395    ($variant:ident: $fmt:literal $(, $arg:expr)* $(,)?) => {
396        $crate::__private::fmt_err($crate::VortexError::$variant, &format_args!($fmt $(, $arg)*))
397    };
398    ($variant:ident: $err:expr $(,)?) => {
399        $crate::__private::must_use(
400            $crate::VortexError::$variant($err)
401        )
402    };
403    ($fmt:literal $(, $arg:expr)* $(,)?) => {
404        $crate::vortex_err!(Other: $fmt, $($arg),*)
405    };
406}
407
408/// A convenience macro for returning a VortexError.
409#[macro_export]
410macro_rules! vortex_bail {
411    ($($tt:tt)+) => {
412        return Err($crate::vortex_err!($($tt)+))
413    };
414}
415
416/// A macro that mirrors `assert!` but instead of panicking on a failed condition,
417/// it will immediately return an erroneous `VortexResult` to the calling context.
418#[macro_export]
419macro_rules! vortex_ensure {
420    ($cond:expr) => {
421        vortex_ensure!($cond, AssertionFailed: "{}", stringify!($cond));
422    };
423    ($cond:expr, $($tt:tt)*) => {
424        if !$cond {
425            $crate::vortex_bail!($($tt)*);
426        }
427    };
428}
429
430/// A macro that mirrors `assert_eq!` but instead of panicking when left != right,
431/// it will immediately return an erroneous `VortexResult` to the calling context.
432#[macro_export]
433macro_rules! vortex_ensure_eq {
434    ($left:expr, $right:expr) => {
435        $crate::vortex_ensure_eq!($left, $right, AssertionFailed: "{} != {}: {:?} != {:?}", stringify!($left), stringify!($right), $left, $right);
436    };
437    ($left:expr, $right:expr, $($tt:tt)*) => {
438        if $left != $right {
439            $crate::vortex_bail!($($tt)*);
440        }
441    };
442}
443
444/// A convenient macro for panicking with a VortexError in the presence of a programmer error
445/// (e.g., an invariant has been violated).
446#[macro_export]
447macro_rules! vortex_panic {
448    (OutOfBounds: $idx:expr, $start:expr, $stop:expr) => {{
449        $crate::vortex_panic!($crate::vortex_err!(OutOfBounds: $idx, $start, $stop))
450    }};
451    (NotImplemented: $func:expr, $for_whom:expr) => {{
452        $crate::vortex_panic!($crate::vortex_err!(NotImplemented: $func, $for_whom))
453    }};
454    (MismatchedTypes: $expected:literal, $actual:expr) => {{
455        $crate::vortex_panic!($crate::vortex_err!(MismatchedTypes: $expected, $actual))
456    }};
457    (MismatchedTypes: $expected:expr, $actual:expr) => {{
458        $crate::vortex_panic!($crate::vortex_err!(MismatchedTypes: $expected, $actual))
459    }};
460    (Context: $msg:literal, $err:expr) => {{
461        $crate::vortex_panic!($crate::vortex_err!(Context: $msg, $err))
462    }};
463    ($variant:ident: $fmt:literal $(, $arg:expr)* $(,)?) => {
464        $crate::vortex_panic!($crate::vortex_err!($variant: $fmt, $($arg),*))
465    };
466    ($err:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
467        $crate::__private::panic_err_ctx($err, &format_args!($fmt $(, $arg)*))
468    };
469    ($fmt:literal $(, $arg:expr)* $(,)?) => {
470        $crate::__private::panic_fmt(&format_args!($fmt $(, $arg)*))
471    };
472    ($err:expr) => {
473        $crate::__private::panic_err($err)
474    };
475}
476
477impl From<arrow_schema::ArrowError> for VortexError {
478    fn from(value: arrow_schema::ArrowError) -> Self {
479        VortexError::Arrow(value, Box::new(Backtrace::capture()))
480    }
481}
482
483#[cfg(feature = "flatbuffers")]
484impl From<flatbuffers::InvalidFlatbuffer> for VortexError {
485    fn from(value: flatbuffers::InvalidFlatbuffer) -> Self {
486        VortexError::FlatBuffers(value, Box::new(Backtrace::capture()))
487    }
488}
489
490impl From<io::Error> for VortexError {
491    fn from(value: io::Error) -> Self {
492        VortexError::Io(value, Box::new(Backtrace::capture()))
493    }
494}
495
496#[cfg(feature = "object_store")]
497impl From<object_store::Error> for VortexError {
498    fn from(value: object_store::Error) -> Self {
499        VortexError::ObjectStore(value, Box::new(Backtrace::capture()))
500    }
501}
502
503impl From<jiff::Error> for VortexError {
504    fn from(value: jiff::Error) -> Self {
505        VortexError::Jiff(value, Box::new(Backtrace::capture()))
506    }
507}
508
509#[cfg(feature = "tokio")]
510impl From<tokio::task::JoinError> for VortexError {
511    fn from(value: tokio::task::JoinError) -> Self {
512        if value.is_panic() {
513            std::panic::resume_unwind(value.into_panic())
514        } else {
515            VortexError::Join(value, Box::new(Backtrace::capture()))
516        }
517    }
518}
519
520impl From<TryFromIntError> for VortexError {
521    fn from(value: TryFromIntError) -> Self {
522        VortexError::TryFromInt(value, Box::new(Backtrace::capture()))
523    }
524}
525
526impl From<prost::EncodeError> for VortexError {
527    fn from(value: prost::EncodeError) -> Self {
528        Self::Prost(Box::new(value), Box::new(Backtrace::capture()))
529    }
530}
531
532impl From<prost::DecodeError> for VortexError {
533    fn from(value: prost::DecodeError) -> Self {
534        Self::Prost(Box::new(value), Box::new(Backtrace::capture()))
535    }
536}
537
538impl From<prost::UnknownEnumValue> for VortexError {
539    fn from(value: prost::UnknownEnumValue) -> Self {
540        Self::Prost(Box::new(value), Box::new(Backtrace::capture()))
541    }
542}
543
544/// We want our error handlers to be fast in the happy path, so we inline them.
545/// However, if we inline them as is, the call site gets the cold branch with
546/// panic! macros which gets expanded into backtrace collection and throwing.
547/// LLVM inliner/vectoriser in turn doesn't vectorize some code.
548/// So we inline only the happy path, the error path gets to a cold handler which
549/// is never inlined and thus doesn't prevent vectorization.
550#[doc(hidden)]
551#[expect(
552    clippy::panic,
553    reason = "cold handlers for vortex_panic!/vortex_expect intentionally panic"
554)]
555pub mod __private {
556    use std::backtrace::Backtrace;
557    use std::error::Error;
558    use std::fmt;
559    use std::fmt::Arguments;
560
561    use crate::ErrString;
562    use crate::VortexError;
563
564    #[doc(hidden)]
565    #[inline]
566    #[cold]
567    #[must_use]
568    pub const fn must_use(error: VortexError) -> VortexError {
569        error
570    }
571
572    #[doc(hidden)]
573    #[cold]
574    #[inline(never)]
575    #[must_use]
576    pub fn fmt_err(
577        variant: fn(ErrString, Box<Backtrace>) -> VortexError,
578        args: &Arguments<'_>,
579    ) -> VortexError {
580        variant(args.to_string().into(), Box::new(Backtrace::capture()))
581    }
582
583    #[doc(hidden)]
584    #[cold]
585    #[inline(never)]
586    #[must_use]
587    pub fn out_of_bounds(idx: usize, start: usize, stop: usize) -> VortexError {
588        VortexError::OutOfBounds(idx, start, stop, Box::new(Backtrace::capture()))
589    }
590
591    #[doc(hidden)]
592    #[cold]
593    #[inline(never)]
594    #[must_use]
595    pub fn not_implemented(func: impl Into<ErrString>, by_whom: &Arguments<'_>) -> VortexError {
596        VortexError::NotImplemented(
597            func.into(),
598            by_whom.to_string().into(),
599            Box::new(Backtrace::capture()),
600        )
601    }
602
603    #[doc(hidden)]
604    #[cold]
605    #[inline(never)]
606    #[must_use]
607    pub fn mismatched_types(expected: &dyn fmt::Display, actual: &dyn fmt::Display) -> VortexError {
608        VortexError::MismatchedTypes(
609            expected.to_string().into(),
610            actual.to_string().into(),
611            Box::new(Backtrace::capture()),
612        )
613    }
614
615    #[doc(hidden)]
616    #[cold]
617    #[inline(never)]
618    #[must_use]
619    pub fn context(msg: &'static str, err: VortexError) -> VortexError {
620        VortexError::Context(msg.into(), Box::new(err))
621    }
622
623    #[doc(hidden)]
624    #[cold]
625    #[inline(never)]
626    #[must_use]
627    pub fn external(err: impl Into<Box<dyn Error + Send + Sync + 'static>>) -> VortexError {
628        VortexError::External(err.into(), Box::new(Backtrace::capture()))
629    }
630
631    #[doc(hidden)]
632    #[cold]
633    #[inline(never)]
634    pub fn panic_err(err: VortexError) -> ! {
635        panic!("{err}")
636    }
637
638    #[doc(hidden)]
639    #[cold]
640    #[inline(never)]
641    pub fn panic_fmt(args: &Arguments<'_>) -> ! {
642        panic!(
643            "{}",
644            VortexError::Other(args.to_string().into(), Box::new(Backtrace::capture()))
645        )
646    }
647
648    #[doc(hidden)]
649    #[cold]
650    #[inline(never)]
651    pub fn panic_err_ctx(err: VortexError, args: &Arguments<'_>) -> ! {
652        panic!("{}", err.with_context(args.to_string()))
653    }
654
655    #[doc(hidden)]
656    #[cold]
657    #[inline(never)]
658    pub fn result_expect_failed(err: impl Into<VortexError>, msg: &'static str) -> ! {
659        panic!("{}", err.into().with_context(msg))
660    }
661
662    #[doc(hidden)]
663    #[cold]
664    #[inline(never)]
665    pub fn option_expect_failed(msg: &'static str) -> ! {
666        panic!(
667            "{}",
668            VortexError::AssertionFailed(msg.into(), Box::new(Backtrace::capture()))
669        )
670    }
671}