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