Skip to main content

boa_engine/error/
mod.rs

1//! Error-related types and conversions.
2
3#[cfg(test)]
4mod tests;
5
6use crate::{
7    Context, JsResult, JsString, JsValue,
8    builtins::{
9        Array,
10        error::{Error, ErrorKind},
11    },
12    js_string,
13    object::JsObject,
14    property::PropertyDescriptor,
15    realm::Realm,
16    vm::{
17        NativeSourceInfo,
18        shadow_stack::{Backtrace, ErrorStack, ShadowEntry},
19    },
20};
21use boa_gc::{Finalize, Trace, custom_trace};
22use std::{borrow::Cow, error, fmt};
23use thiserror::Error;
24
25/// Create an error object from a value or string literal. Optionally the
26/// first argument of the macro can be a type of error (such as `TypeError`,
27/// `RangeError` or `InternalError`).
28///
29/// Can be used with an expression that converts into `JsValue` or a format
30/// string with arguments.
31///
32/// # Native Errors
33///
34/// The only native error that is not buildable using this macro is
35/// `AggregateError`, which requires multiple error objects available at
36/// construction.
37///
38/// [`InternalError`][mdn] is non-standard and unsupported in Boa.
39///
40/// All other native error types can be created from the macro using their
41/// JavaScript name followed by a colon, like:
42///
43/// ```ignore
44/// js_error!(TypeError: "hello world");
45/// ```
46///
47/// # Examples
48///
49/// ```
50/// # use boa_engine::{js_str, Context, JsValue};
51/// use boa_engine::js_error;
52/// let context = &mut Context::default();
53///
54/// let error = js_error!("error!");
55/// assert!(error.as_opaque().is_some());
56/// assert_eq!(
57///     error.as_opaque().unwrap().to_string(context).unwrap(),
58///     "error!"
59/// );
60///
61/// let error = js_error!("error: {}", 5);
62/// assert_eq!(
63///     error.as_opaque().unwrap().to_string(context).unwrap(),
64///     "error: 5"
65/// );
66///
67/// // Non-string literals must be used as an expression.
68/// let error = js_error!({ true });
69/// assert_eq!(error.as_opaque().unwrap(), &JsValue::from(true));
70/// ```
71///
72/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/InternalError
73#[macro_export]
74macro_rules! js_error {
75    (Error: $value: literal) => {
76        $crate::JsError::from_native(
77            $crate::JsNativeError::error().with_message($value)
78        )
79    };
80    (Error: $value: literal $(, $args: expr)* $(,)?) => {
81        $crate::JsError::from_native(
82            $crate::JsNativeError::error()
83                .with_message(format!($value $(, $args)*))
84        )
85    };
86
87    (TypeError: $value: literal) => {
88        $crate::JsError::from_native(
89            $crate::JsNativeError::typ().with_message($value)
90        )
91    };
92    (TypeError: $value: literal $(, $args: expr)* $(,)?) => {
93        $crate::JsError::from_native(
94            $crate::JsNativeError::typ()
95                .with_message(format!($value $(, $args)*))
96        )
97    };
98
99    (SyntaxError: $value: literal) => {
100        $crate::JsError::from_native(
101            $crate::JsNativeError::syntax().with_message($value)
102        )
103    };
104    (SyntaxError: $value: literal $(, $args: expr)* $(,)?) => {
105        $crate::JsError::from_native(
106            $crate::JsNativeError::syntax().with_message(format!($value $(, $args)*))
107        )
108    };
109
110    (RangeError: $value: literal) => {
111        $crate::JsError::from_native(
112            $crate::JsNativeError::range().with_message($value)
113        )
114    };
115    (RangeError: $value: literal $(, $args: expr)* $(,)?) => {
116        $crate::JsError::from_native(
117            $crate::JsNativeError::range().with_message(format!($value $(, $args)*))
118        )
119    };
120
121    (EvalError: $value: literal) => {
122        $crate::JsError::from_native(
123            $crate::JsNativeError::eval().with_message($value)
124        )
125    };
126    (EvalError: $value: literal $(, $args: expr)* $(,)?) => {
127        $crate::JsError::from_native(
128            $crate::JsNativeError::eval().with_message(format!($value $(, $args)*))
129        )
130    };
131
132    (ReferenceError: $value: literal) => {
133        $crate::JsError::from_native(
134            $crate::JsNativeError::reference().with_message($value)
135        )
136    };
137    (ReferenceError: $value: literal $(, $args: expr)* $(,)?) => {
138        $crate::JsError::from_native(
139            $crate::JsNativeError::reference().with_message(format!($value $(, $args)*))
140        )
141    };
142
143    (URIError: $value: literal) => {
144        $crate::JsError::from_native(
145            $crate::JsNativeError::uri().with_message($value)
146        )
147    };
148    (URIError: $value: literal $(, $args: expr)* $(,)?) => {
149        $crate::JsError::from_native(
150            $crate::JsNativeError::uri().with_message(format!($value $(, $args)*))
151        )
152    };
153
154    ($value: literal) => {
155        $crate::JsError::from_opaque($crate::JsValue::from(
156            $crate::js_string!($value)
157        ))
158    };
159    ($value: expr) => {
160        $crate::JsError::from_opaque(
161            $crate::JsValue::from($value)
162        )
163    };
164    ($value: literal $(, $args: expr)* $(,)?) => {
165        $crate::JsError::from_opaque($crate::JsValue::from(
166            $crate::JsString::from(format!($value $(, $args)*))
167        ))
168    };
169}
170
171/// The error type returned by all operations related
172/// to the execution of Javascript code.
173///
174/// This is essentially an enum that can store either [`JsNativeError`]s (for ideal
175/// native errors)  or opaque [`JsValue`]s, since Javascript allows throwing any valid
176/// `JsValue`.
177///
178/// The implementation doesn't provide a [`From`] conversion
179/// for `JsValue`. This is with the intent of encouraging the usage of proper
180/// `JsNativeError`s instead of plain `JsValue`s. However, if you
181/// do need a proper opaque error, you can construct one using the
182/// [`JsError::from_opaque`] method.
183///
184/// # Examples
185///
186/// ```rust
187/// # use boa_engine::{JsError, JsNativeError, JsNativeErrorKind, JsValue, js_string};
188/// let cause = JsError::from_opaque(js_string!("error!").into());
189///
190/// assert!(cause.as_opaque().is_some());
191/// assert_eq!(
192///     cause.as_opaque().unwrap(),
193///     &JsValue::from(js_string!("error!"))
194/// );
195///
196/// let native_error: JsError = JsNativeError::typ()
197///     .with_message("invalid type!")
198///     .with_cause(cause)
199///     .into();
200///
201/// let native = native_error.as_native().unwrap();
202/// let kind = native.kind();
203/// assert!(matches!(kind, JsNativeErrorKind::Type));
204/// ```
205#[derive(Debug, Clone, Trace, Finalize)]
206#[boa_gc(unsafe_no_drop)]
207pub struct JsError {
208    inner: Repr,
209    pub(crate) backtrace: Option<Backtrace>,
210}
211
212impl Eq for JsError {}
213impl PartialEq for JsError {
214    /// The backtrace information is ignored.
215    #[inline]
216    fn eq(&self, other: &Self) -> bool {
217        // NOTE: We want to ignore stack trace, since that is
218        //       meta-information about the error.
219        self.inner == other.inner
220    }
221}
222
223/// Internal representation of a [`JsError`].
224///
225/// `JsError` is represented by an opaque enum because it restricts
226/// matching against `JsError` without calling `try_native` first.
227/// This allows us to provide a safe API for `Error` objects that extracts
228/// their info as a native `Rust` type ([`JsNativeError`]).
229///
230/// This should never be used outside of this module. If that's not the case,
231/// you should add methods to either `JsError` or `JsNativeError` to
232/// represent that special use case.
233#[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize)]
234#[boa_gc(unsafe_no_drop)]
235enum Repr {
236    Opaque(JsValue),
237    Native(Box<JsNativeError>),
238    Engine(EngineError),
239}
240
241impl error::Error for JsError {
242    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
243        match &self.inner {
244            Repr::Native(err) => err.source(),
245            Repr::Opaque(_) => None,
246            Repr::Engine(err) => err.source(),
247        }
248    }
249}
250
251/// The error type returned by the [`JsError::try_native`] method.
252#[derive(Debug, Clone, Error)]
253pub enum TryNativeError {
254    /// A property of the error object has an invalid type.
255    #[error("invalid type of property `{0}`")]
256    InvalidPropertyType(&'static str),
257
258    /// The message of the error object could not be decoded.
259    #[error("property `message` cannot contain unpaired surrogates")]
260    InvalidMessageEncoding,
261
262    /// The constructor property of the error object was invalid.
263    #[error("invalid `constructor` property of Error object")]
264    InvalidConstructor,
265
266    /// A property of the error object is not accessible.
267    #[error("could not access property `{property}`")]
268    InaccessibleProperty {
269        /// The name of the property that could not be accessed.
270        property: &'static str,
271
272        /// The source error.
273        source: JsError,
274    },
275
276    /// An inner error of an aggregate error is not accessible.
277    #[error("could not get element `{index}` of property `errors`")]
278    InvalidErrorsIndex {
279        /// The index of the error that could not be accessed.
280        index: u64,
281
282        /// The source error.
283        source: JsError,
284    },
285
286    /// The error value is not an error object.
287    #[error("opaque error of type `{:?}` is not an Error object", .0.get_type())]
288    NotAnErrorObject(JsValue),
289
290    /// The original realm of the error object was inaccessible.
291    #[error("could not access realm of Error object")]
292    InaccessibleRealm {
293        /// The source error.
294        source: JsError,
295    },
296    /// The error was an engine error.
297    #[error("could not convert engine error into a native error")]
298    EngineError {
299        /// The source error.
300        source: EngineError,
301    },
302}
303
304/// Runtime limit related errors.
305#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Error, Trace, Finalize)]
306#[boa_gc(empty_trace)]
307pub enum RuntimeLimitError {
308    /// Error for reaching the iteration loops limit.
309    #[error("reached the maximum number of iteration loops on this execution")]
310    LoopIteration,
311    /// Error for reaching the maximum amount of recursive calls.
312    #[error("reached the maximum number of recursive calls on this execution")]
313    Recursion,
314    /// Error for reaching the maximum stack size
315    #[error("reached the maximum stack size on this execution")]
316    StackSize,
317    /// The embedding host canceled execution.
318    #[error("execution interrupted by the host")]
319    Interrupted,
320}
321
322/// Internal panic error.
323#[derive(Debug, Clone, Error, Eq, PartialEq, Trace, Finalize)]
324#[boa_gc(unsafe_no_drop)]
325#[error("{message}")]
326#[must_use]
327pub struct PanicError {
328    /// The original panic message providing context about what went wrong.
329    message: Box<str>,
330    /// The source error of this panic, if applicable.
331    source: Option<Box<JsError>>,
332}
333
334impl PanicError {
335    /// Creates a `PanicError` error from a panic message.
336    pub fn new<S: Into<Box<str>>>(message: S) -> Self {
337        PanicError {
338            message: message.into(),
339            source: None,
340        }
341    }
342
343    /// Sets the source error of this `PanicError`.
344    pub fn with_source<E: Into<JsError>>(mut self, source: E) -> Self {
345        self.source = Some(Box::new(source.into()));
346        self
347    }
348
349    /// Gets the message of this `PanicError`.
350    #[must_use]
351    pub fn message(&self) -> &str {
352        &self.message
353    }
354}
355
356impl From<PanicError> for JsError {
357    fn from(err: PanicError) -> Self {
358        EngineError::from(err).into()
359    }
360}
361
362/// Engine error that cannot be caught from within ECMAScript code.
363#[derive(Debug, Clone, Error, Eq, PartialEq, Trace, Finalize)]
364#[boa_gc(unsafe_no_drop)]
365#[allow(variant_size_differences)]
366pub enum EngineError {
367    /// Error thrown when no instructions remain. Only used in a fuzzing context.
368    #[cfg(feature = "fuzz")]
369    #[error("NoInstructionsRemainError: instruction budget was exhausted")]
370    NoInstructionsRemain,
371
372    /// Error thrown when a runtime limit is exceeded.
373    #[error("RuntimeLimitError: {0}")]
374    RuntimeLimit(#[from] RuntimeLimitError),
375
376    /// Error thrown when an internal panic condition is encountered.
377    #[error("EnginePanic: {0}")]
378    Panic(#[from] PanicError),
379}
380
381impl EngineError {
382    /// Converts this error into its thread-safe, erased version.
383    ///
384    /// Even though this operation is lossy, converting into an `ErasedEngineError`
385    /// is useful since it implements `Send` and `Sync`, making it compatible with
386    /// error reporting frameworks such as `anyhow`, `eyre` or `miette`.
387    fn into_erased(self, context: &mut Context) -> ErasedEngineError {
388        match self {
389            #[cfg(feature = "fuzz")]
390            EngineError::NoInstructionsRemain => ErasedEngineError::NoInstructionsRemain,
391            EngineError::RuntimeLimit(err) => ErasedEngineError::RuntimeLimit(err),
392            EngineError::Panic(err) => ErasedEngineError::Panic(ErasedPanicError {
393                message: err.message,
394                source: err.source.map(|err| Box::new(err.into_erased(context))),
395            }),
396        }
397    }
398}
399
400impl JsError {
401    /// Creates a new `JsError` from a native error `err`.
402    ///
403    /// # Examples
404    ///
405    /// ```rust
406    /// # use boa_engine::{JsError, JsNativeError};
407    /// let error = JsError::from_native(JsNativeError::syntax());
408    ///
409    /// assert!(error.as_native().is_some());
410    /// ```
411    #[must_use]
412    pub fn from_native(err: JsNativeError) -> Self {
413        Self {
414            inner: Repr::Native(Box::new(err)),
415            backtrace: None,
416        }
417    }
418
419    /// Creates a new `JsError` from a Rust standard error `err`.
420    /// This will create a new `JsNativeError` with the message of the standard error.
421    ///
422    /// # Examples
423    ///
424    /// ```
425    /// # use boa_engine::JsError;
426    /// let error = std::io::Error::new(std::io::ErrorKind::Other, "oh no!");
427    /// let js_error: JsError = JsError::from_rust(error);
428    ///
429    /// assert_eq!(js_error.as_native().unwrap().message(), "oh no!");
430    /// assert!(js_error.as_native().unwrap().cause().is_none());
431    /// ```
432    #[must_use]
433    pub fn from_rust(err: impl error::Error) -> Self {
434        let mut native_err = JsNativeError::error().with_message(err.to_string());
435        if let Some(source) = err.source() {
436            native_err = native_err.with_cause(Self::from_rust(source));
437        }
438
439        Self::from_native(native_err)
440    }
441
442    /// Creates a new `JsError` from an opaque error `value`.
443    ///
444    /// # Examples
445    ///
446    /// ```rust
447    /// # use boa_engine::JsError;
448    /// let error = JsError::from_opaque(5.0f64.into());
449    ///
450    /// assert!(error.as_opaque().is_some());
451    /// ```
452    #[must_use]
453    pub fn from_opaque(value: JsValue) -> Self {
454        // Recover the backtrace from the Error object if present,
455        // so it survives the JsError → JsValue → JsError round-trip.
456        let backtrace = value.as_object().and_then(|obj| {
457            let error = obj.downcast_ref::<Error>()?;
458            error.stack.0.backtrace().cloned()
459        });
460        Self {
461            inner: Repr::Opaque(value),
462            backtrace,
463        }
464    }
465
466    /// Converts the error to an opaque `JsValue` error
467    ///
468    /// Unwraps the inner `JsValue` if the error is already an opaque error.
469    ///
470    /// # Errors
471    ///
472    /// Returns the original error if `self` was an engine error.
473    ///
474    /// # Examples
475    ///
476    /// ```rust
477    /// # use boa_engine::{Context, JsError, JsNativeError};
478    /// # use boa_engine::builtins::error::Error;
479    /// # use boa_engine::error::{EngineError, RuntimeLimitError};
480    /// let context = &mut Context::default();
481    /// let error: JsError =
482    ///     JsNativeError::eval().with_message("invalid script").into();
483    /// let error_val = error.into_opaque(context).unwrap();
484    ///
485    /// assert!(error_val.as_object().unwrap().is::<Error>());
486    ///
487    /// let error: JsError =
488    ///     EngineError::RuntimeLimit(RuntimeLimitError::Recursion).into();
489    ///
490    /// assert!(error.into_opaque(context).is_err());
491    /// ```
492    pub fn into_opaque(self, context: &mut Context) -> JsResult<JsValue> {
493        match self.inner {
494            Repr::Native(e) => {
495                let obj = e.into_opaque(context);
496                // Store the backtrace in the Error object so it survives the
497                // JsError → JsValue → JsError round-trip through promise
498                // rejection.
499                if let Some(backtrace) = self.backtrace
500                    && let Some(mut error) = obj.downcast_mut::<Error>()
501                {
502                    error.stack = IgnoreEq(ErrorStack::Backtrace(backtrace));
503                }
504                Ok(obj.into())
505            }
506            Repr::Opaque(v) => {
507                // Store the backtrace in the Error object for opaque errors
508                // too (e.g. explicit `throw new Error(...)`).
509                if let Some(backtrace) = self.backtrace
510                    && let Some(obj) = v.as_object()
511                    && let Some(mut error) = obj.downcast_mut::<Error>()
512                    && !error.stack.0.is_backtrace()
513                {
514                    error.stack = IgnoreEq(ErrorStack::Backtrace(backtrace));
515                }
516                Ok(v.clone())
517            }
518            Repr::Engine(_) => Err(self),
519        }
520    }
521
522    /// Unwraps the inner error if this contains a native error.
523    /// Otherwise, inspects the opaque error and tries to extract the
524    /// necessary information to construct a native error similar to the provided
525    /// opaque error. If the conversion fails, returns a [`TryNativeError`]
526    /// with the cause of the failure.
527    ///
528    /// # Note 1
529    ///
530    /// This method won't try to make any conversions between JS types.
531    /// In other words, for this conversion to succeed:
532    /// - `message` **MUST** be a `JsString` value.
533    /// - `errors` (in the case of `AggregateError`s) **MUST** be an `Array` object.
534    ///
535    /// # Note 2
536    ///
537    /// This operation should be considered a lossy conversion, since it
538    /// won't store any additional properties of the opaque
539    /// error, other than `message`, `cause` and `errors` (in the case of
540    /// `AggregateError`s). If you cannot afford a lossy conversion, clone
541    /// the object before calling [`from_opaque`][JsError::from_opaque]
542    /// to preserve its original properties.
543    ///
544    /// # Examples
545    ///
546    /// ```rust
547    /// # use boa_engine::{Context, JsError, JsNativeError, JsNativeErrorKind};
548    /// let context = &mut Context::default();
549    ///
550    /// // create a new, opaque Error object
551    /// let error: JsError = JsNativeError::typ().with_message("type error!").into();
552    /// let error_val = error.into_opaque(context).unwrap();
553    ///
554    /// // then, try to recover the original
555    /// let error = JsError::from_opaque(error_val).try_native(context).unwrap();
556    ///
557    /// assert!(matches!(error.kind(), JsNativeErrorKind::Type));
558    /// assert_eq!(error.message(), "type error!");
559    /// ```
560    pub fn try_native(&self, context: &mut Context) -> Result<JsNativeError, TryNativeError> {
561        match &self.inner {
562            Repr::Engine(e) => Err(TryNativeError::EngineError { source: e.clone() }),
563            Repr::Native(e) => Ok(e.as_ref().clone()),
564            Repr::Opaque(val) => {
565                let obj = val
566                    .as_object()
567                    .ok_or_else(|| TryNativeError::NotAnErrorObject(val.clone()))?;
568                let error_data: Error = obj
569                    .downcast_ref::<Error>()
570                    .ok_or_else(|| TryNativeError::NotAnErrorObject(val.clone()))?
571                    .clone();
572
573                let try_get_property = |key: JsString, name, context: &mut Context| {
574                    obj.try_get(key, context)
575                        .map_err(|e| TryNativeError::InaccessibleProperty {
576                            property: name,
577                            source: e,
578                        })
579                };
580
581                let message = if let Some(msg) =
582                    try_get_property(js_string!("message"), "message", context)?
583                {
584                    Cow::Owned(
585                        msg.as_string()
586                            .as_ref()
587                            .map(JsString::to_std_string)
588                            .transpose()
589                            .map_err(|_| TryNativeError::InvalidMessageEncoding)?
590                            .ok_or(TryNativeError::InvalidPropertyType("message"))?,
591                    )
592                } else {
593                    Cow::Borrowed("")
594                };
595
596                let cause = try_get_property(js_string!("cause"), "cause", context)?;
597
598                let location = error_data.stack.clone();
599                let kind = match error_data.tag {
600                    ErrorKind::Error => JsNativeErrorKind::Error,
601                    ErrorKind::Eval => JsNativeErrorKind::Eval,
602                    ErrorKind::Type => JsNativeErrorKind::Type,
603                    ErrorKind::Range => JsNativeErrorKind::Range,
604                    ErrorKind::Reference => JsNativeErrorKind::Reference,
605                    ErrorKind::Syntax => JsNativeErrorKind::Syntax,
606                    ErrorKind::Uri => JsNativeErrorKind::Uri,
607                    ErrorKind::Aggregate => {
608                        let errors = obj.get(js_string!("errors"), context).map_err(|e| {
609                            TryNativeError::InaccessibleProperty {
610                                property: "errors",
611                                source: e,
612                            }
613                        })?;
614                        let mut error_list = Vec::new();
615                        match errors.as_object() {
616                            Some(errors) if errors.is_array() => {
617                                let length = errors.length_of_array_like(context).map_err(|e| {
618                                    TryNativeError::InaccessibleProperty {
619                                        property: "errors.length",
620                                        source: e,
621                                    }
622                                })?;
623                                error_list.reserve(length as usize);
624                                for i in 0..length {
625                                    error_list.push(Self::from_opaque(
626                                        errors.get(i, context).map_err(|e| {
627                                            TryNativeError::InvalidErrorsIndex {
628                                                index: i,
629                                                source: e,
630                                            }
631                                        })?,
632                                    ));
633                                }
634                            }
635                            _ => return Err(TryNativeError::InvalidPropertyType("errors")),
636                        }
637
638                        JsNativeErrorKind::Aggregate(error_list)
639                    }
640                };
641
642                let realm = try_get_property(js_string!("constructor"), "constructor", context)?
643                    .as_ref()
644                    .and_then(JsValue::as_constructor)
645                    .ok_or(TryNativeError::InvalidConstructor)?
646                    .get_function_realm(context)
647                    .map_err(|err| TryNativeError::InaccessibleRealm { source: err })?;
648
649                Ok(JsNativeError {
650                    kind,
651                    message,
652                    cause: cause.map(|v| Box::new(Self::from_opaque(v))),
653                    realm: Some(realm),
654                    stack: location,
655                })
656            }
657        }
658    }
659
660    /// Gets the inner [`JsValue`] if the error is an opaque error,
661    /// or `None` otherwise.
662    ///
663    /// # Examples
664    ///
665    /// ```rust
666    /// # use boa_engine::{JsError, JsNativeError};
667    /// let error: JsError = JsNativeError::reference()
668    ///     .with_message("variable not found!")
669    ///     .into();
670    ///
671    /// assert!(error.as_opaque().is_none());
672    ///
673    /// let error = JsError::from_opaque(256u32.into());
674    ///
675    /// assert!(error.as_opaque().is_some());
676    /// ```
677    #[must_use]
678    pub const fn as_opaque(&self) -> Option<&JsValue> {
679        match self.inner {
680            Repr::Native(_) | Repr::Engine(_) => None,
681            Repr::Opaque(ref v) => Some(v),
682        }
683    }
684
685    /// Gets the inner [`JsNativeError`] if the error is a native
686    /// error, or `None` otherwise.
687    ///
688    /// # Examples
689    ///
690    /// ```rust
691    /// # use boa_engine::{JsError, JsNativeError, JsValue};
692    /// let error: JsError =
693    ///     JsNativeError::error().with_message("Unknown error").into();
694    ///
695    /// assert!(error.as_native().is_some());
696    ///
697    /// let error = JsError::from_opaque(JsValue::undefined());
698    ///
699    /// assert!(error.as_native().is_none());
700    /// ```
701    #[must_use]
702    pub const fn as_native(&self) -> Option<&JsNativeError> {
703        match &self.inner {
704            Repr::Native(e) => Some(e),
705            Repr::Opaque(_) | Repr::Engine(_) => None,
706        }
707    }
708
709    /// Gets the inner [`JsNativeError`] if the error is a native
710    /// error, or `None` otherwise.
711    #[must_use]
712    pub const fn as_native_mut(&mut self) -> Option<&mut JsNativeError> {
713        match &mut self.inner {
714            Repr::Native(e) => Some(e),
715            Repr::Opaque(_) | Repr::Engine(_) => None,
716        }
717    }
718
719    /// Gets the inner [`EngineError`] if the error is an engine
720    #[must_use]
721    pub const fn as_engine(&self) -> Option<&EngineError> {
722        match &self.inner {
723            Repr::Opaque(_) | Repr::Native(_) => None,
724            Repr::Engine(err) => Some(err),
725        }
726    }
727
728    /// Converts this error into its thread-safe, erased version.
729    ///
730    /// Even though this operation is lossy, converting into a `JsErasedError`
731    /// is useful since it implements `Send` and `Sync`, making it compatible with
732    /// error reporting frameworks such as `anyhow`, `eyre` or `miette`.
733    ///
734    /// # Examples
735    ///
736    /// ```rust
737    /// # use boa_engine::{js_string, Context, JsError, JsNativeError, JsSymbol, JsValue};
738    /// # use std::error::Error;
739    /// let context = &mut Context::default();
740    /// let cause = JsError::from_opaque(JsSymbol::new(Some(js_string!("error!"))).unwrap().into());
741    ///
742    /// let native_error: JsError = JsNativeError::typ()
743    ///     .with_message("invalid type!")
744    ///     .with_cause(cause)
745    ///     .into();
746    ///
747    /// let erased_error = native_error.into_erased(context);
748    ///
749    /// assert_eq!(erased_error.to_string(), "TypeError: invalid type!");
750    ///
751    /// let send_sync_error: Box<dyn Error + Send + Sync> = Box::new(erased_error);
752    ///
753    /// assert_eq!(
754    ///     send_sync_error.source().unwrap().to_string(),
755    ///     "Symbol(error!)"
756    /// );
757    /// ```
758    pub fn into_erased(self, context: &mut Context) -> JsErasedError {
759        let native = match self.try_native(context) {
760            Ok(native) => native,
761            Err(TryNativeError::EngineError { source }) => {
762                return JsErasedError {
763                    inner: ErasedRepr::Engine(source.into_erased(context)),
764                };
765            }
766            Err(_) => {
767                return JsErasedError {
768                    inner: ErasedRepr::Opaque(Cow::Owned(self.to_string())),
769                };
770            }
771        };
772
773        let JsNativeError {
774            kind,
775            message,
776            cause,
777            ..
778        } = native;
779
780        let cause = cause.map(|err| Box::new(err.into_erased(context)));
781
782        let kind = match kind {
783            JsNativeErrorKind::Aggregate(errors) => JsErasedNativeErrorKind::Aggregate(
784                errors
785                    .into_iter()
786                    .map(|err| err.into_erased(context))
787                    .collect(),
788            ),
789            JsNativeErrorKind::Error => JsErasedNativeErrorKind::Error,
790            JsNativeErrorKind::Eval => JsErasedNativeErrorKind::Eval,
791            JsNativeErrorKind::Range => JsErasedNativeErrorKind::Range,
792            JsNativeErrorKind::Reference => JsErasedNativeErrorKind::Reference,
793            JsNativeErrorKind::Syntax => JsErasedNativeErrorKind::Syntax,
794            JsNativeErrorKind::Type => JsErasedNativeErrorKind::Type,
795            JsNativeErrorKind::Uri => JsErasedNativeErrorKind::Uri,
796        };
797
798        JsErasedError {
799            inner: ErasedRepr::Native(JsErasedNativeError {
800                kind,
801                message,
802                cause,
803            }),
804        }
805    }
806
807    /// Injects a realm on the `realm` field of a native error.
808    ///
809    /// This is a no-op if the error is not native or if the `realm` field of the error is already
810    /// set.
811    pub(crate) fn inject_realm(mut self, realm: Realm) -> Self {
812        match &mut self.inner {
813            Repr::Native(err) if err.realm.is_none() => {
814                err.realm = Some(realm);
815            }
816            _ => {}
817        }
818        self
819    }
820
821    /// Is the [`JsError`] catchable in JavaScript.
822    #[inline]
823    pub(crate) const fn is_catchable(&self) -> bool {
824        self.as_engine().is_none()
825    }
826}
827
828impl From<boa_parser::Error> for JsError {
829    #[cfg_attr(feature = "native-backtrace", track_caller)]
830    fn from(err: boa_parser::Error) -> Self {
831        Self::from(JsNativeError::from(err))
832    }
833}
834
835impl From<JsNativeError> for JsError {
836    fn from(error: JsNativeError) -> Self {
837        Self {
838            inner: Repr::Native(Box::new(error)),
839            backtrace: None,
840        }
841    }
842}
843
844impl From<EngineError> for JsError {
845    fn from(value: EngineError) -> Self {
846        Self {
847            inner: Repr::Engine(value),
848            backtrace: None,
849        }
850    }
851}
852
853impl From<RuntimeLimitError> for JsError {
854    fn from(value: RuntimeLimitError) -> Self {
855        EngineError::from(value).into()
856    }
857}
858
859impl fmt::Display for JsError {
860    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
861        match &self.inner {
862            Repr::Native(e) => e.fmt(f)?,
863            Repr::Engine(e) => e.fmt(f)?,
864            Repr::Opaque(v) => v.display().fmt(f)?,
865        }
866
867        if let Some(shadow_stack) = &self.backtrace {
868            for entry in shadow_stack.iter().rev() {
869                write!(f, "\n    at {}", entry.display(true))?;
870            }
871        }
872        Ok(())
873    }
874}
875
876/// Helper struct that ignores equality operator.
877#[derive(Debug, Clone, Finalize)]
878pub(crate) struct IgnoreEq<T>(pub(crate) T);
879
880impl<T> Eq for IgnoreEq<T> {}
881
882impl<T> PartialEq for IgnoreEq<T> {
883    #[inline]
884    fn eq(&self, _: &Self) -> bool {
885        true
886    }
887}
888
889impl<T> std::hash::Hash for IgnoreEq<T> {
890    fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
891}
892
893impl<T> From<T> for IgnoreEq<T> {
894    #[inline]
895    fn from(value: T) -> Self {
896        Self(value)
897    }
898}
899
900/// Native representation of an ideal `Error` object from Javascript.
901///
902/// This representation is more space efficient than its [`JsObject`] equivalent,
903/// since it doesn't need to create a whole new `JsObject` to be instantiated.
904/// Prefer using this over [`JsError`] when you don't need to throw
905/// plain [`JsValue`]s as errors, or when you need to inspect the error type
906/// of a `JsError`.
907///
908/// # Examples
909///
910/// ```rust
911/// # use boa_engine::{JsNativeError, JsNativeErrorKind};
912/// let native_error = JsNativeError::uri().with_message("cannot decode uri");
913///
914/// match native_error.kind() {
915///     JsNativeErrorKind::Uri => { /* handle URI error*/ }
916///     _ => unreachable!(),
917/// }
918///
919/// assert_eq!(native_error.message(), "cannot decode uri");
920/// ```
921#[derive(Clone, Finalize, Error, PartialEq, Eq)]
922pub struct JsNativeError {
923    /// The kind of native error (e.g. `TypeError`, `SyntaxError`, etc.)
924    kind: JsNativeErrorKind,
925    message: Cow<'static, str>,
926    #[source]
927    cause: Option<Box<JsError>>,
928    realm: Option<Realm>,
929    pub(crate) stack: IgnoreEq<ErrorStack>,
930}
931
932impl fmt::Display for JsNativeError {
933    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
934        write!(f, "{}", self.kind)?;
935
936        let message = self.message.trim();
937        if !message.is_empty() {
938            write!(f, ": {message}")?;
939        }
940
941        if let Some(entry) = self.stack.0.position() {
942            write!(f, "{}", entry.display(false))?;
943        }
944
945        Ok(())
946    }
947}
948
949// SAFETY: just mirroring the default derive to allow destructuring.
950unsafe impl Trace for JsNativeError {
951    custom_trace!(this, mark, {
952        mark(&this.kind);
953        mark(&this.cause);
954        mark(&this.realm);
955    });
956}
957
958impl fmt::Debug for JsNativeError {
959    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
960        f.debug_struct("JsNativeError")
961            .field("kind", &self.kind)
962            .field("message", &self.message)
963            .field("cause", &self.cause)
964            .finish_non_exhaustive()
965    }
966}
967
968impl JsNativeError {
969    /// Returns the kind of this native error.
970    #[must_use]
971    #[inline]
972    pub const fn kind(&self) -> &JsNativeErrorKind {
973        &self.kind
974    }
975
976    /// Creates a new `JsNativeError` from its `kind`, `message` and (optionally) its `cause`.
977    #[cfg_attr(feature = "native-backtrace", track_caller)]
978    const fn new(
979        kind: JsNativeErrorKind,
980        message: Cow<'static, str>,
981        cause: Option<Box<JsError>>,
982    ) -> Self {
983        if let Some(cause) = &cause
984            && cause.as_engine().is_some()
985        {
986            panic!("engine errors cannot be used as the cause of another error");
987        }
988        Self {
989            kind,
990            message,
991            cause,
992            realm: None,
993            stack: IgnoreEq(ErrorStack::Position(ShadowEntry::Native {
994                function_name: None,
995                source_info: NativeSourceInfo::caller(),
996            })),
997        }
998    }
999
1000    /// Creates a new `JsNativeError` of kind `AggregateError` from a list of [`JsError`]s, with
1001    /// empty `message` and undefined `cause`.
1002    ///
1003    /// # Examples
1004    ///
1005    /// ```rust
1006    /// # use boa_engine::{JsNativeError, JsNativeErrorKind};
1007    /// let inner_errors = vec![
1008    ///     JsNativeError::typ().into(),
1009    ///     JsNativeError::syntax().into()
1010    /// ];
1011    /// let error = JsNativeError::aggregate(inner_errors);
1012    ///
1013    /// assert!(matches!(
1014    ///     error.kind(),
1015    ///     JsNativeErrorKind::Aggregate(errors) if errors.len() == 2
1016    /// ));
1017    /// ```
1018    #[must_use]
1019    #[inline]
1020    #[cfg_attr(feature = "native-backtrace", track_caller)]
1021    pub const fn aggregate(errors: Vec<JsError>) -> Self {
1022        Self::new(
1023            JsNativeErrorKind::Aggregate(errors),
1024            Cow::Borrowed(""),
1025            None,
1026        )
1027    }
1028
1029    /// Check if it's a [`JsNativeErrorKind::Aggregate`].
1030    #[must_use]
1031    #[inline]
1032    pub const fn is_aggregate(&self) -> bool {
1033        matches!(self.kind, JsNativeErrorKind::Aggregate(_))
1034    }
1035
1036    /// Creates a new `JsNativeError` of kind `Error`, with empty `message` and undefined `cause`.
1037    ///
1038    /// # Examples
1039    ///
1040    /// ```rust
1041    /// # use boa_engine::{JsNativeError, JsNativeErrorKind};
1042    /// let error = JsNativeError::error();
1043    ///
1044    /// assert!(matches!(error.kind(), JsNativeErrorKind::Error));
1045    /// ```
1046    #[must_use]
1047    #[inline]
1048    #[cfg_attr(feature = "native-backtrace", track_caller)]
1049    pub const fn error() -> Self {
1050        Self::new(JsNativeErrorKind::Error, Cow::Borrowed(""), None)
1051    }
1052
1053    /// Check if it's a [`JsNativeErrorKind::Error`].
1054    #[must_use]
1055    #[inline]
1056    pub const fn is_error(&self) -> bool {
1057        matches!(self.kind, JsNativeErrorKind::Error)
1058    }
1059
1060    /// Creates a new `JsNativeError` of kind `EvalError`, with empty `message` and undefined `cause`.
1061    ///
1062    /// # Examples
1063    ///
1064    /// ```rust
1065    /// # use boa_engine::{JsNativeError, JsNativeErrorKind};
1066    /// let error = JsNativeError::eval();
1067    ///
1068    /// assert!(matches!(error.kind(), JsNativeErrorKind::Eval));
1069    /// ```
1070    #[must_use]
1071    #[inline]
1072    #[cfg_attr(feature = "native-backtrace", track_caller)]
1073    pub const fn eval() -> Self {
1074        Self::new(JsNativeErrorKind::Eval, Cow::Borrowed(""), None)
1075    }
1076
1077    /// Check if it's a [`JsNativeErrorKind::Eval`].
1078    #[must_use]
1079    #[inline]
1080    pub const fn is_eval(&self) -> bool {
1081        matches!(self.kind, JsNativeErrorKind::Eval)
1082    }
1083
1084    /// Creates a new `JsNativeError` of kind `RangeError`, with empty `message` and undefined `cause`.
1085    ///
1086    /// # Examples
1087    ///
1088    /// ```rust
1089    /// # use boa_engine::{JsNativeError, JsNativeErrorKind};
1090    /// let error = JsNativeError::range();
1091    ///
1092    /// assert!(matches!(error.kind(), JsNativeErrorKind::Range));
1093    /// ```
1094    #[must_use]
1095    #[inline]
1096    #[cfg_attr(feature = "native-backtrace", track_caller)]
1097    pub const fn range() -> Self {
1098        Self::new(JsNativeErrorKind::Range, Cow::Borrowed(""), None)
1099    }
1100
1101    /// Check if it's a [`JsNativeErrorKind::Range`].
1102    #[must_use]
1103    #[inline]
1104    pub const fn is_range(&self) -> bool {
1105        matches!(self.kind, JsNativeErrorKind::Range)
1106    }
1107
1108    /// Creates a new `JsNativeError` of kind `ReferenceError`, with empty `message` and undefined `cause`.
1109    ///
1110    /// # Examples
1111    ///
1112    /// ```rust
1113    /// # use boa_engine::{JsNativeError, JsNativeErrorKind};
1114    /// let error = JsNativeError::reference();
1115    ///
1116    /// assert!(matches!(error.kind(), JsNativeErrorKind::Reference));
1117    /// ```
1118    #[must_use]
1119    #[inline]
1120    #[cfg_attr(feature = "native-backtrace", track_caller)]
1121    pub const fn reference() -> Self {
1122        Self::new(JsNativeErrorKind::Reference, Cow::Borrowed(""), None)
1123    }
1124
1125    /// Check if it's a [`JsNativeErrorKind::Reference`].
1126    #[must_use]
1127    #[inline]
1128    pub const fn is_reference(&self) -> bool {
1129        matches!(self.kind, JsNativeErrorKind::Reference)
1130    }
1131
1132    /// Creates a new `JsNativeError` of kind `SyntaxError`, with empty `message` and undefined `cause`.
1133    ///
1134    /// # Examples
1135    ///
1136    /// ```rust
1137    /// # use boa_engine::{JsNativeError, JsNativeErrorKind};
1138    /// let error = JsNativeError::syntax();
1139    ///
1140    /// assert!(matches!(error.kind(), JsNativeErrorKind::Syntax));
1141    /// ```
1142    #[must_use]
1143    #[inline]
1144    #[cfg_attr(feature = "native-backtrace", track_caller)]
1145    pub const fn syntax() -> Self {
1146        Self::new(JsNativeErrorKind::Syntax, Cow::Borrowed(""), None)
1147    }
1148
1149    /// Check if it's a [`JsNativeErrorKind::Syntax`].
1150    #[must_use]
1151    #[inline]
1152    pub const fn is_syntax(&self) -> bool {
1153        matches!(self.kind, JsNativeErrorKind::Syntax)
1154    }
1155
1156    /// Creates a new `JsNativeError` of kind `TypeError`, with empty `message` and undefined `cause`.
1157    ///
1158    /// # Examples
1159    ///
1160    /// ```rust
1161    /// # use boa_engine::{JsNativeError, JsNativeErrorKind};
1162    /// let error = JsNativeError::typ();
1163    ///
1164    /// assert!(matches!(error.kind(), JsNativeErrorKind::Type));
1165    /// ```
1166    #[must_use]
1167    #[inline]
1168    #[cfg_attr(feature = "native-backtrace", track_caller)]
1169    pub const fn typ() -> Self {
1170        Self::new(JsNativeErrorKind::Type, Cow::Borrowed(""), None)
1171    }
1172
1173    /// Check if it's a [`JsNativeErrorKind::Type`].
1174    #[must_use]
1175    #[inline]
1176    pub const fn is_type(&self) -> bool {
1177        matches!(self.kind, JsNativeErrorKind::Type)
1178    }
1179
1180    /// Creates a new `JsNativeError` of kind `UriError`, with empty `message` and undefined `cause`.
1181    ///
1182    /// # Examples
1183    ///
1184    /// ```rust
1185    /// # use boa_engine::{JsNativeError, JsNativeErrorKind};
1186    /// let error = JsNativeError::uri();
1187    ///
1188    /// assert!(matches!(error.kind(), JsNativeErrorKind::Uri));
1189    /// ```
1190    #[must_use]
1191    #[inline]
1192    #[cfg_attr(feature = "native-backtrace", track_caller)]
1193    pub const fn uri() -> Self {
1194        Self::new(JsNativeErrorKind::Uri, Cow::Borrowed(""), None)
1195    }
1196
1197    /// Check if it's a [`JsNativeErrorKind::Uri`].
1198    #[must_use]
1199    #[inline]
1200    pub const fn is_uri(&self) -> bool {
1201        matches!(self.kind, JsNativeErrorKind::Uri)
1202    }
1203
1204    /// Sets the message of this error.
1205    ///
1206    /// # Examples
1207    ///
1208    /// ```rust
1209    /// # use boa_engine::JsNativeError;
1210    /// let error = JsNativeError::range().with_message("number too large");
1211    ///
1212    /// assert_eq!(error.message(), "number too large");
1213    /// ```
1214    #[must_use]
1215    #[inline]
1216    pub fn with_message<S>(mut self, message: S) -> Self
1217    where
1218        S: Into<Cow<'static, str>>,
1219    {
1220        self.message = message.into();
1221        self
1222    }
1223
1224    /// Sets the cause of this error.
1225    ///
1226    /// # Examples
1227    ///
1228    /// ```rust
1229    /// # use boa_engine::JsNativeError;
1230    /// let cause = JsNativeError::syntax();
1231    /// let error = JsNativeError::error().with_cause(cause);
1232    ///
1233    /// assert!(error.cause().unwrap().as_native().is_some());
1234    /// ```
1235    ///
1236    /// # Panics
1237    ///
1238    /// Panics if `cause` is an uncatchable error (i.e. an engine error).
1239    #[must_use]
1240    #[inline]
1241    pub fn with_cause<V>(mut self, cause: V) -> Self
1242    where
1243        V: Into<JsError>,
1244    {
1245        let err = cause.into();
1246        assert!(
1247            err.is_catchable(),
1248            "uncatchable errors cannot be used as the cause of another error",
1249        );
1250
1251        self.cause = Some(Box::new(err));
1252        self
1253    }
1254
1255    /// Gets the `message` of this error.
1256    ///
1257    /// This is equivalent to the [`NativeError.prototype.message`][spec]
1258    /// property.
1259    ///
1260    /// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message
1261    ///
1262    /// # Examples
1263    ///
1264    /// ```rust
1265    /// # use boa_engine::JsNativeError;
1266    /// let error = JsNativeError::range().with_message("number too large");
1267    ///
1268    /// assert_eq!(error.message(), "number too large");
1269    /// ```
1270    #[must_use]
1271    #[inline]
1272    pub fn message(&self) -> &str {
1273        &self.message
1274    }
1275
1276    /// Gets the `cause` of this error.
1277    ///
1278    /// This is equivalent to the [`NativeError.prototype.cause`][spec]
1279    /// property.
1280    ///
1281    /// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
1282    ///
1283    /// # Examples
1284    ///
1285    /// ```rust
1286    /// # use boa_engine::JsNativeError;
1287    /// let cause = JsNativeError::syntax();
1288    /// let error = JsNativeError::error().with_cause(cause);
1289    ///
1290    /// assert!(error.cause().unwrap().as_native().is_some());
1291    /// ```
1292    #[must_use]
1293    #[inline]
1294    pub fn cause(&self) -> Option<&JsError> {
1295        self.cause.as_deref()
1296    }
1297
1298    /// Converts this native error to its opaque representation as a [`JsObject`].
1299    ///
1300    /// # Examples
1301    ///
1302    /// ```rust
1303    /// # use boa_engine::{Context, JsError, JsNativeError, js_string};
1304    /// # use boa_engine::builtins::error::Error;
1305    /// let context = &mut Context::default();
1306    ///
1307    /// let error = JsNativeError::error().with_message("error!");
1308    /// let error_obj = error.into_opaque(context);
1309    ///
1310    /// assert!(error_obj.is::<Error>());
1311    /// assert_eq!(
1312    ///     error_obj.get(js_string!("message"), context).unwrap(),
1313    ///     js_string!("error!").into()
1314    /// )
1315    /// ```
1316    #[inline]
1317    pub fn into_opaque(self, context: &mut Context) -> JsObject {
1318        let Self {
1319            kind,
1320            message,
1321            cause,
1322            realm,
1323            stack,
1324        } = self;
1325        let constructors = realm.as_ref().map_or_else(
1326            || context.intrinsics().constructors(),
1327            |realm| realm.intrinsics().constructors(),
1328        );
1329        let (prototype, tag) = match kind {
1330            JsNativeErrorKind::Aggregate(_) => (
1331                constructors.aggregate_error().prototype(),
1332                ErrorKind::Aggregate,
1333            ),
1334            JsNativeErrorKind::Error => (constructors.error().prototype(), ErrorKind::Error),
1335            JsNativeErrorKind::Eval => (constructors.eval_error().prototype(), ErrorKind::Eval),
1336            JsNativeErrorKind::Range => (constructors.range_error().prototype(), ErrorKind::Range),
1337            JsNativeErrorKind::Reference => (
1338                constructors.reference_error().prototype(),
1339                ErrorKind::Reference,
1340            ),
1341            JsNativeErrorKind::Syntax => {
1342                (constructors.syntax_error().prototype(), ErrorKind::Syntax)
1343            }
1344            JsNativeErrorKind::Type => (constructors.type_error().prototype(), ErrorKind::Type),
1345            JsNativeErrorKind::Uri => (constructors.uri_error().prototype(), ErrorKind::Uri),
1346        };
1347
1348        let o = JsObject::from_proto_and_data_with_shared_shape(
1349            context.root_shape(),
1350            prototype,
1351            Error::with_stack(tag, stack.0.clone()),
1352        )
1353        .upcast();
1354
1355        o.create_non_enumerable_data_property_or_throw(
1356            js_string!("message"),
1357            js_string!(message.as_ref()),
1358            context,
1359        );
1360
1361        if let Some(cause) = cause {
1362            o.create_non_enumerable_data_property_or_throw(
1363                js_string!("cause"),
1364                cause
1365                    .into_opaque(context)
1366                    .expect("engine errors cannot be the cause of another error"),
1367                context,
1368            );
1369        }
1370
1371        if let JsNativeErrorKind::Aggregate(errors) = kind {
1372            let errors = errors
1373                .into_iter()
1374                .map(|e| {
1375                    e.into_opaque(context)
1376                        .expect("engine errors cannot be the cause of another error")
1377                })
1378                .collect::<Vec<_>>();
1379            let errors = Array::create_array_from_list(errors, context);
1380            o.define_property_or_throw(
1381                js_string!("errors"),
1382                PropertyDescriptor::builder()
1383                    .configurable(true)
1384                    .enumerable(false)
1385                    .writable(true)
1386                    .value(errors),
1387                context,
1388            )
1389            .expect("The spec guarantees this succeeds for a newly created object ");
1390        }
1391        o
1392    }
1393
1394    /// Sets the realm of this error.
1395    pub(crate) fn with_realm(mut self, realm: Realm) -> Self {
1396        self.realm = Some(realm);
1397        self
1398    }
1399}
1400
1401impl From<boa_parser::Error> for JsNativeError {
1402    #[cfg_attr(feature = "native-backtrace", track_caller)]
1403    fn from(err: boa_parser::Error) -> Self {
1404        Self::syntax().with_message(err.to_string())
1405    }
1406}
1407
1408/// The list of possible error types a [`JsNativeError`] can be.
1409///
1410/// More information:
1411/// - [ECMAScript reference][spec]
1412/// - [MDN documentation][mdn]
1413///
1414/// [spec]: https://tc39.es/ecma262/#sec-error-objects
1415/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
1416#[derive(Debug, Clone, Finalize, PartialEq, Eq)]
1417#[non_exhaustive]
1418pub enum JsNativeErrorKind {
1419    /// A collection of errors wrapped in a single error.
1420    ///
1421    /// More information:
1422    /// - [ECMAScript reference][spec]
1423    /// - [MDN documentation][mdn]
1424    ///
1425    /// [spec]: https://tc39.es/ecma262/#sec-aggregate-error-objects
1426    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
1427    Aggregate(Vec<JsError>),
1428    /// A generic error. Commonly used as the base for custom exceptions.
1429    ///
1430    /// More information:
1431    /// - [ECMAScript reference][spec]
1432    /// - [MDN documentation][mdn]
1433    ///
1434    /// [spec]: https://tc39.es/ecma262/#sec-error-constructor
1435    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
1436    Error,
1437    /// An error related to the global function [`eval()`][eval].
1438    ///
1439    /// More information:
1440    /// - [ECMAScript reference][spec]
1441    /// - [MDN documentation][mdn]
1442    ///
1443    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-evalerror
1444    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/EvalError
1445    /// [eval]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
1446    Eval,
1447    /// An error thrown when a value is outside its valid range.
1448    ///
1449    /// More information:
1450    /// - [ECMAScript reference][spec]
1451    /// - [MDN documentation][mdn]
1452    ///
1453    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror
1454    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError
1455    Range,
1456    /// An error representing an invalid de-reference of a variable.
1457    ///
1458    /// More information:
1459    /// - [ECMAScript reference][spec]
1460    /// - [MDN documentation][mdn]
1461    ///
1462    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-referenceerror
1463    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError
1464    Reference,
1465    /// An error representing an invalid syntax in the Javascript language.
1466    ///
1467    /// More information:
1468    /// - [ECMAScript reference][spec]
1469    /// - [MDN documentation][mdn]
1470    ///
1471    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-syntaxerror
1472    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError
1473    Syntax,
1474    /// An error thrown when a variable or argument is not of a valid type.
1475    ///
1476    /// More information:
1477    /// - [ECMAScript reference][spec]
1478    /// - [MDN documentation][mdn]
1479    ///
1480    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-typeerror
1481    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
1482    Type,
1483    /// An error thrown when the [`encodeURI()`][e_uri] and [`decodeURI()`][d_uri] functions receive
1484    /// invalid parameters.
1485    ///
1486    /// More information:
1487    /// - [ECMAScript reference][spec]
1488    /// - [MDN documentation][mdn]
1489    ///
1490    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-urierror
1491    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
1492    /// [e_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
1493    /// [d_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI
1494    Uri,
1495}
1496
1497// SAFETY: just mirroring the default derive to allow destructuring.
1498unsafe impl Trace for JsNativeErrorKind {
1499    custom_trace!(
1500        this,
1501        mark,
1502        match &this {
1503            Self::Aggregate(errors) => mark(errors),
1504            Self::Error
1505            | Self::Eval
1506            | Self::Range
1507            | Self::Reference
1508            | Self::Syntax
1509            | Self::Type
1510            | Self::Uri => {}
1511        }
1512    );
1513}
1514
1515impl PartialEq<ErrorKind> for JsNativeErrorKind {
1516    fn eq(&self, other: &ErrorKind) -> bool {
1517        matches!(
1518            (self, other),
1519            (Self::Aggregate(_), ErrorKind::Aggregate)
1520                | (Self::Error, ErrorKind::Error)
1521                | (Self::Eval, ErrorKind::Eval)
1522                | (Self::Range, ErrorKind::Range)
1523                | (Self::Reference, ErrorKind::Reference)
1524                | (Self::Syntax, ErrorKind::Syntax)
1525                | (Self::Type, ErrorKind::Type)
1526                | (Self::Uri, ErrorKind::Uri)
1527        )
1528    }
1529}
1530
1531impl fmt::Display for JsNativeErrorKind {
1532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1533        match self {
1534            Self::Aggregate(_) => "AggregateError",
1535            Self::Error => "Error",
1536            Self::Eval => "EvalError",
1537            Self::Range => "RangeError",
1538            Self::Reference => "ReferenceError",
1539            Self::Syntax => "SyntaxError",
1540            Self::Type => "TypeError",
1541            Self::Uri => "UriError",
1542        }
1543        .fmt(f)
1544    }
1545}
1546
1547/// Erased version of [`PanicError`].
1548///
1549/// This is mainly useful to convert a `PanicError` into an `ErasedPanicError` that also
1550/// implements `Send + Sync`, which makes it compatible with error reporting tools
1551/// such as `anyhow`, `eyre` or `miette`.
1552///
1553/// Generally, the conversion from `PanicError` to `ErasedPanicError` is unidirectional,
1554/// since any `JsError` that is a [`JsValue`] is converted to its string representation
1555/// instead. This will lose information if that value was an object, a symbol or a big int.
1556#[derive(Debug, Clone, Error, Eq, PartialEq, Trace, Finalize)]
1557#[error("{message}")]
1558#[must_use]
1559pub struct ErasedPanicError {
1560    message: Box<str>,
1561    source: Option<Box<JsErasedError>>,
1562}
1563
1564impl ErasedPanicError {
1565    /// Gets the message of this `ErasedPanicError`.
1566    #[must_use]
1567    pub fn message(&self) -> &str {
1568        &self.message
1569    }
1570}
1571
1572/// Erased version of [`EngineError`].
1573///
1574/// This is mainly useful to convert an `EngineError` into an `ErasedEngineError` that also
1575/// implements `Send + Sync`, which makes it compatible with error reporting tools
1576/// such as `anyhow`, `eyre` or `miette`.
1577///
1578/// Generally, the conversion from `EngineError` to `ErasedEngineError` is unidirectional,
1579/// since any `JsError` that is a [`JsValue`] is converted to its string representation
1580/// instead. This will lose information if that value was an object, a symbol or a big int.
1581#[derive(Debug, Clone, Error, Eq, PartialEq, Trace, Finalize)]
1582#[allow(variant_size_differences)]
1583pub enum ErasedEngineError {
1584    /// Error thrown when no instructions remain. Only used in a fuzzing context.
1585    #[cfg(feature = "fuzz")]
1586    #[error("NoInstructionsRemainError: instruction budget was exhausted")]
1587    NoInstructionsRemain,
1588
1589    /// Error thrown when a runtime limit is exceeded.
1590    #[error("RuntimeLimitError: {0}")]
1591    RuntimeLimit(#[from] RuntimeLimitError),
1592
1593    /// Error thrown when an internal panic condition is encountered.
1594    #[error("EnginePanic: {0}")]
1595    Panic(#[from] ErasedPanicError),
1596}
1597
1598/// Erased version of [`JsError`].
1599///
1600/// This is mainly useful to convert a `JsError` into an `Error` that also
1601/// implements `Send + Sync`, which makes it compatible with error reporting tools
1602/// such as `anyhow`, `eyre` or `miette`.
1603///
1604/// Generally, the conversion from `JsError` to `JsErasedError` is unidirectional,
1605/// since any `JsError` that is a [`JsValue`] is converted to its string representation
1606/// instead. This will lose information if that value was an object, a symbol or a big int.
1607#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
1608pub struct JsErasedError {
1609    inner: ErasedRepr,
1610}
1611
1612#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
1613enum ErasedRepr {
1614    Native(JsErasedNativeError),
1615    Opaque(Cow<'static, str>),
1616    Engine(ErasedEngineError),
1617}
1618
1619impl fmt::Display for JsErasedError {
1620    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1621        match &self.inner {
1622            ErasedRepr::Native(e) => e.fmt(f),
1623            ErasedRepr::Opaque(v) => v.fmt(f),
1624            ErasedRepr::Engine(e) => e.fmt(f),
1625        }
1626    }
1627}
1628
1629impl error::Error for JsErasedError {
1630    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
1631        match &self.inner {
1632            ErasedRepr::Native(err) => err.source(),
1633            ErasedRepr::Opaque(_) => None,
1634            ErasedRepr::Engine(err) => err.source(),
1635        }
1636    }
1637}
1638
1639impl JsErasedError {
1640    /// Gets the inner [`str`] if the error is an opaque error,
1641    /// or `None` otherwise.
1642    #[must_use]
1643    pub fn as_opaque(&self) -> Option<&str> {
1644        match &self.inner {
1645            ErasedRepr::Native(_) | ErasedRepr::Engine(_) => None,
1646            ErasedRepr::Opaque(v) => Some(v),
1647        }
1648    }
1649
1650    /// Gets the inner [`JsErasedNativeError`] if the error is a native
1651    /// error, or `None` otherwise.
1652    #[must_use]
1653    pub const fn as_native(&self) -> Option<&JsErasedNativeError> {
1654        match &self.inner {
1655            ErasedRepr::Native(e) => Some(e),
1656            ErasedRepr::Opaque(_) | ErasedRepr::Engine(_) => None,
1657        }
1658    }
1659
1660    /// Gets the inner [`ErasedEngineError`] if the error is an engine
1661    /// error, or `None` otherwise.
1662    #[must_use]
1663    pub const fn as_engine(&self) -> Option<&ErasedEngineError> {
1664        match &self.inner {
1665            ErasedRepr::Engine(e) => Some(e),
1666            ErasedRepr::Opaque(_) | ErasedRepr::Native(_) => None,
1667        }
1668    }
1669}
1670
1671/// Erased version of [`JsNativeError`].
1672#[derive(Debug, Clone, Trace, Finalize, Error, PartialEq, Eq)]
1673pub struct JsErasedNativeError {
1674    /// The kind of native error (e.g. `TypeError`, `SyntaxError`, etc.)
1675    pub kind: JsErasedNativeErrorKind,
1676    message: Cow<'static, str>,
1677    #[source]
1678    cause: Option<Box<JsErasedError>>,
1679}
1680
1681impl fmt::Display for JsErasedNativeError {
1682    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1683        write!(f, "{}", self.kind)?;
1684
1685        let message = self.message.trim();
1686        if !message.is_empty() {
1687            write!(f, ": {message}")?;
1688        }
1689
1690        Ok(())
1691    }
1692}
1693
1694/// Erased version of [`JsNativeErrorKind`]
1695#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
1696#[non_exhaustive]
1697pub enum JsErasedNativeErrorKind {
1698    /// A collection of errors wrapped in a single error.
1699    ///
1700    /// More information:
1701    /// - [ECMAScript reference][spec]
1702    /// - [MDN documentation][mdn]
1703    ///
1704    /// [spec]: https://tc39.es/ecma262/#sec-aggregate-error-objects
1705    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
1706    Aggregate(Vec<JsErasedError>),
1707    /// A generic error. Commonly used as the base for custom exceptions.
1708    ///
1709    /// More information:
1710    /// - [ECMAScript reference][spec]
1711    /// - [MDN documentation][mdn]
1712    ///
1713    /// [spec]: https://tc39.es/ecma262/#sec-error-constructor
1714    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
1715    Error,
1716    /// An error related to the global function [`eval()`][eval].
1717    ///
1718    /// More information:
1719    /// - [ECMAScript reference][spec]
1720    /// - [MDN documentation][mdn]
1721    ///
1722    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-evalerror
1723    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/EvalError
1724    /// [eval]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
1725    Eval,
1726    /// An error thrown when a value is outside its valid range.
1727    ///
1728    /// More information:
1729    /// - [ECMAScript reference][spec]
1730    /// - [MDN documentation][mdn]
1731    ///
1732    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror
1733    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError
1734    Range,
1735    /// An error representing an invalid de-reference of a variable.
1736    ///
1737    /// More information:
1738    /// - [ECMAScript reference][spec]
1739    /// - [MDN documentation][mdn]
1740    ///
1741    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-referenceerror
1742    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError
1743    Reference,
1744    /// An error representing an invalid syntax in the Javascript language.
1745    ///
1746    /// More information:
1747    /// - [ECMAScript reference][spec]
1748    /// - [MDN documentation][mdn]
1749    ///
1750    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-syntaxerror
1751    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError
1752    Syntax,
1753    /// An error thrown when a variable or argument is not of a valid type.
1754    ///
1755    /// More information:
1756    /// - [ECMAScript reference][spec]
1757    /// - [MDN documentation][mdn]
1758    ///
1759    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-typeerror
1760    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
1761    Type,
1762    /// An error thrown when the [`encodeURI()`][e_uri] and [`decodeURI()`][d_uri] functions receive
1763    /// invalid parameters.
1764    ///
1765    /// More information:
1766    /// - [ECMAScript reference][spec]
1767    /// - [MDN documentation][mdn]
1768    ///
1769    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-urierror
1770    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
1771    /// [e_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
1772    /// [d_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI
1773    Uri,
1774
1775    /// Error thrown when a runtime limit is exceeded.
1776    RuntimeLimit,
1777}
1778
1779impl fmt::Display for JsErasedNativeErrorKind {
1780    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1781        match &self {
1782            Self::Aggregate(errors) => {
1783                return write!(f, "AggregateError(error count: {})", errors.len());
1784            }
1785            Self::Error => "Error",
1786            Self::Eval => "EvalError",
1787            Self::Range => "RangeError",
1788            Self::Reference => "ReferenceError",
1789            Self::Syntax => "SyntaxError",
1790            Self::Type => "TypeError",
1791            Self::Uri => "UriError",
1792            Self::RuntimeLimit => "RuntimeLimit",
1793        }
1794        .fmt(f)
1795    }
1796}