Skip to main content

boa_engine/builtins/error/
mod.rs

1//! Boa's implementation of ECMAScript's global `Error` object.
2//!
3//! Error objects are thrown when runtime errors occur.
4//! The Error object can also be used as a base object for user-defined exceptions.
5//!
6//! More information:
7//!  - [MDN documentation][mdn]
8//!  - [ECMAScript reference][spec]
9//!
10//! [spec]: https://tc39.es/ecma262/#sec-error-objects
11//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
12
13use std::fmt::Write;
14
15use crate::{
16    Context, JsArgs, JsData, JsResult, JsString, JsValue,
17    builtins::BuiltInObject,
18    context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
19    error::{IgnoreEq, JsNativeError},
20    js_string,
21    object::{JsObject, internal_methods::get_prototype_from_constructor},
22    property::Attribute,
23    realm::Realm,
24    string::StaticJsStrings,
25    vm::{
26        NativeSourceInfo,
27        shadow_stack::{ErrorStack, ShadowEntry},
28    },
29};
30use boa_gc::{Finalize, Trace};
31use boa_macros::js_str;
32
33pub(crate) mod aggregate;
34pub(crate) mod eval;
35pub(crate) mod range;
36pub(crate) mod reference;
37pub(crate) mod syntax;
38pub(crate) mod r#type;
39pub(crate) mod uri;
40
41#[cfg(test)]
42mod tests;
43
44pub(crate) use self::aggregate::AggregateError;
45pub(crate) use self::eval::EvalError;
46pub(crate) use self::range::RangeError;
47pub(crate) use self::reference::ReferenceError;
48pub(crate) use self::syntax::SyntaxError;
49pub(crate) use self::r#type::TypeError;
50pub(crate) use self::uri::UriError;
51
52use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject};
53
54/// A tag of built-in `Error` object, [ECMAScript spec][spec].
55///
56/// [spec]: https://tc39.es/ecma262/#sec-error-objects
57#[derive(Debug, Copy, Clone, Eq, PartialEq, Trace, Finalize, JsData)]
58#[boa_gc(empty_trace)]
59#[non_exhaustive]
60pub enum ErrorKind {
61    /// The `AggregateError` object type.
62    ///
63    /// More information:
64    ///  - [ECMAScript reference][spec]
65    ///
66    /// [spec]: https://tc39.es/ecma262/#sec-aggregate-error-objects
67    Aggregate,
68
69    /// The `Error` object type.
70    ///
71    /// More information:
72    ///  - [ECMAScript reference][spec]
73    ///
74    /// [spec]: https://tc39.es/ecma262/#sec-error-objects
75    Error,
76
77    /// The `EvalError` type.
78    ///
79    /// More information:
80    ///  - [ECMAScript reference][spec]
81    ///
82    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-evalerror
83    Eval,
84
85    /// The `TypeError` type.
86    ///
87    /// More information:
88    ///  - [ECMAScript reference][spec]
89    ///
90    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-typeerror
91    Type,
92
93    /// The `RangeError` type.
94    ///
95    /// More information:
96    ///  - [ECMAScript reference][spec]
97    ///
98    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror
99    Range,
100
101    /// The `ReferenceError` type.
102    ///
103    /// More information:
104    ///  - [ECMAScript reference][spec]
105    ///
106    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-referenceerror
107    Reference,
108
109    /// The `SyntaxError` type.
110    ///
111    /// More information:
112    ///  - [ECMAScript reference][spec]
113    ///
114    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-syntaxerror
115    Syntax,
116
117    /// The `URIError` type.
118    ///
119    /// More information:
120    ///  - [ECMAScript reference][spec]
121    ///
122    /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-urierror
123    Uri,
124}
125
126/// A built-in `Error` object, per the [ECMAScript spec][spec].
127///
128/// This is used internally to convert between [`JsObject`] and
129/// [`JsNativeError`] correctly, but it can also be used to manually create `Error`
130/// objects. However, the recommended way to create them is to construct a
131/// `JsNativeError` first, then call [`JsNativeError::into_opaque`],
132/// which will assign its prototype, properties and kind automatically.
133///
134/// For a description of every error kind and its usage, see
135/// [`JsNativeErrorKind`][crate::error::JsNativeErrorKind].
136///
137/// [spec]: https://tc39.es/ecma262/#sec-error-objects
138#[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize, JsData)]
139pub struct Error {
140    pub(crate) tag: ErrorKind,
141
142    // The position of where the Error was created does not affect equality check.
143    #[unsafe_ignore_trace]
144    pub(crate) stack: IgnoreEq<ErrorStack>,
145}
146
147impl Error {
148    /// Create a new [`Error`].
149    #[inline]
150    #[must_use]
151    #[cfg_attr(feature = "native-backtrace", track_caller)]
152    pub fn new(tag: ErrorKind) -> Self {
153        Self {
154            tag,
155            stack: IgnoreEq(ErrorStack::Position(ShadowEntry::Native {
156                function_name: None,
157                source_info: NativeSourceInfo::caller(),
158            })),
159        }
160    }
161
162    /// Create a new [`Error`] with the given [`ErrorStack`].
163    pub(crate) fn with_stack(tag: ErrorKind, location: ErrorStack) -> Self {
164        Self {
165            tag,
166            stack: IgnoreEq(location),
167        }
168    }
169
170    /// Get the position from the last called function.
171    pub(crate) fn with_caller_position(tag: ErrorKind, context: &Context) -> Self {
172        let limit = context.runtime_limits().backtrace_limit();
173        let backtrace = context.vm.shadow_stack.caller_position(limit);
174        Self {
175            tag,
176            stack: IgnoreEq(ErrorStack::Backtrace(backtrace)),
177        }
178    }
179}
180
181impl IntrinsicObject for Error {
182    fn init(realm: &Realm) {
183        let property_attribute =
184            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
185        let accessor_attribute = Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
186
187        let get_stack = BuiltInBuilder::callable(realm, Self::get_stack)
188            .name(js_string!("get stack"))
189            .build();
190
191        let set_stack = BuiltInBuilder::callable(realm, Self::set_stack)
192            .name(js_string!("set stack"))
193            .build();
194
195        let builder = BuiltInBuilder::from_standard_constructor::<Self>(realm)
196            .property(js_string!("name"), Self::NAME, property_attribute)
197            .property(js_string!("message"), js_string!(), property_attribute)
198            .method(Self::to_string, js_string!("toString"), 0)
199            .accessor(
200                js_string!("stack"),
201                Some(get_stack),
202                Some(set_stack),
203                accessor_attribute,
204            );
205
206        #[cfg(feature = "experimental")]
207        let builder = builder.static_method(Error::is_error, js_string!("isError"), 1);
208
209        builder.build();
210    }
211
212    fn get(intrinsics: &Intrinsics) -> JsObject {
213        Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
214    }
215}
216
217impl BuiltInObject for Error {
218    const NAME: JsString = StaticJsStrings::ERROR;
219}
220
221impl BuiltInConstructor for Error {
222    const CONSTRUCTOR_ARGUMENTS: usize = 1;
223    const PROTOTYPE_STORAGE_SLOTS: usize = 5;
224    const CONSTRUCTOR_STORAGE_SLOTS: usize = 1;
225
226    const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
227        StandardConstructors::error;
228
229    /// `Error( message [ , options ] )`
230    ///
231    /// Creates a new error object.
232    fn constructor(
233        new_target: &JsValue,
234        args: &[JsValue],
235        context: &mut Context,
236    ) -> JsResult<JsValue> {
237        // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
238        let new_target = &if new_target.is_undefined() {
239            context
240                .active_function_object()
241                .unwrap_or_else(|| context.intrinsics().constructors().error().constructor())
242                .into()
243        } else {
244            new_target.clone()
245        };
246
247        // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%Error.prototype%", « [[ErrorData]] »).
248        let prototype =
249            get_prototype_from_constructor(new_target, StandardConstructors::error, context)?;
250        let o = JsObject::from_proto_and_data_with_shared_shape(
251            context.root_shape(),
252            prototype,
253            Error::with_caller_position(ErrorKind::Error, context),
254        )
255        .upcast();
256
257        // 3. If message is not undefined, then
258        let message = args.get_or_undefined(0);
259        if !message.is_undefined() {
260            // a. Let msg be ? ToString(message).
261            let msg = message.to_string(context)?;
262
263            // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
264            o.create_non_enumerable_data_property_or_throw(js_string!("message"), msg, context);
265        }
266
267        // 4. Perform ? InstallErrorCause(O, options).
268        Self::install_error_cause(&o, args.get_or_undefined(1), context)?;
269
270        // 5. Return O.
271        Ok(o.into())
272    }
273}
274
275impl Error {
276    pub(crate) fn install_error_cause(
277        o: &JsObject,
278        options: &JsValue,
279        context: &mut Context,
280    ) -> JsResult<()> {
281        // 1. If Type(options) is Object and ? HasProperty(options, "cause") is true, then
282        // 1.a. Let cause be ? Get(options, "cause").
283        if let Some(options) = options.as_object()
284            && let Some(cause) = options.try_get(js_string!("cause"), context)?
285        {
286            // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "cause", cause).
287            o.create_non_enumerable_data_property_or_throw(js_string!("cause"), cause, context);
288        }
289
290        // 2. Return unused.
291        Ok(())
292    }
293
294    /// `get Error.prototype.stack`
295    ///
296    /// The accessor property of Error instances represents the stack trace
297    /// when the error was created.
298    ///
299    /// More information:
300    ///  - [Proposal][spec]
301    ///
302    /// [spec]: https://tc39.es/proposal-error-stacks/
303    #[allow(clippy::unnecessary_wraps)]
304    fn get_stack(this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
305        // 1. Let E be the this value.
306        // 2. If E is not an Object, return undefined.
307        let Some(e) = this.as_object() else {
308            return Ok(JsValue::undefined());
309        };
310
311        // 3. Let errorData be the value of the [[ErrorData]] internal slot of E.
312        // 4. If errorData is undefined, return undefined.
313        let Some(error_data) = e.downcast_ref::<Error>() else {
314            return Ok(JsValue::undefined());
315        };
316
317        // 5. Let stackString be an implementation-defined String value representing the call stack.
318        // 6. Return stackString.
319        if let Some(backtrace) = error_data.stack.0.backtrace() {
320            let stack_string = backtrace
321                .iter()
322                .rev()
323                .fold(String::new(), |mut output, entry| {
324                    let _ = writeln!(&mut output, "    at {}", entry.display(true));
325                    output
326                });
327            return Ok(js_string!(stack_string).into());
328        }
329
330        // 7. If no stack trace is available, return undefined.
331        Ok(JsValue::undefined())
332    }
333
334    /// `set Error.prototype.stack`
335    ///
336    /// The setter for the stack property.
337    ///
338    /// More information:
339    ///  - [Proposal][spec]
340    ///
341    /// [spec]: https://tc39.es/proposal-error-stacks/
342    fn set_stack(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
343        // 1. Let E be the this value.
344        // 2. If Type(E) is not Object, throw a TypeError exception.
345        let e = this.as_object().ok_or_else(|| {
346            JsNativeError::typ()
347                .with_message("Error.prototype.stack setter requires that 'this' be an Object")
348        })?;
349
350        // 3. Let numberOfArgs be the number of arguments passed to this function call.
351        // 4. If numberOfArgs is 0, throw a TypeError exception.
352        let Some(value) = args.first() else {
353            return Err(JsNativeError::typ()
354                .with_message(
355                    "Error.prototype.stack setter requires at least 1 argument, but only 0 were passed",
356                )
357                .into());
358        };
359
360        // 5. Return ? CreateDataPropertyOrThrow(E, "stack", value).
361        e.create_data_property_or_throw(js_string!("stack"), value.clone(), context)
362            .map(Into::into)
363    }
364
365    /// `Error.prototype.toString()`
366    ///
367    /// The `toString()` method returns a string representing the specified Error object.
368    ///
369    /// More information:
370    ///  - [MDN documentation][mdn]
371    ///  - [ECMAScript reference][spec]
372    ///
373    /// [spec]: https://tc39.es/ecma262/#sec-error.prototype.tostring
374    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString
375    #[allow(clippy::wrong_self_convention)]
376    pub(crate) fn to_string(
377        this: &JsValue,
378        _: &[JsValue],
379        context: &mut Context,
380    ) -> JsResult<JsValue> {
381        // 1. Let O be the this value.
382        // 2. If Type(O) is not Object, throw a TypeError exception.
383        let o = this
384            .as_object()
385            .ok_or_else(|| JsNativeError::typ().with_message("'this' is not an Object"))?;
386
387        // 3. Let name be ? Get(O, "name").
388        let name = o.get(js_string!("name"), context)?;
389
390        // 4. If name is undefined, set name to "Error"; otherwise set name to ? ToString(name).
391        let name = if name.is_undefined() {
392            js_string!("Error")
393        } else {
394            name.to_string(context)?
395        };
396
397        // 5. Let msg be ? Get(O, "message").
398        let msg = o.get(js_string!("message"), context)?;
399
400        // 6. If msg is undefined, set msg to the empty String; otherwise set msg to ? ToString(msg).
401        let msg = if msg.is_undefined() {
402            js_string!()
403        } else {
404            msg.to_string(context)?
405        };
406
407        // 7. If name is the empty String, return msg.
408        if name.is_empty() {
409            return Ok(msg.into());
410        }
411
412        // 8. If msg is the empty String, return name.
413        if msg.is_empty() {
414            return Ok(name.into());
415        }
416
417        // 9. Return the string-concatenation of name, the code unit 0x003A (COLON),
418        // the code unit 0x0020 (SPACE), and msg.
419        Ok(js_string!(&name, js_str!(": "), &msg).into())
420    }
421
422    /// [`Error.isError`][spec].
423    ///
424    /// Returns a boolean indicating whether the argument is a built-in Error instance or not.
425    ///
426    /// [spec]: https://tc39.es/proposal-is-error/#sec-error.iserror
427    #[cfg(feature = "experimental")]
428    #[allow(clippy::unnecessary_wraps)]
429    fn is_error(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
430        // 1. Return IsError(arg).
431
432        // https://tc39.es/proposal-is-error/#sec-iserror
433
434        // 1. If argument is not an Object, return false.
435        // 2. If argument has an [[ErrorData]] internal slot, return true.
436        // 3. Return false.
437        Ok(args
438            .get_or_undefined(0)
439            .as_object()
440            .is_some_and(|o| o.is::<Error>())
441            .into())
442    }
443
444    /// Shared constructor logic for all `NativeError` subtypes.
445    ///
446    /// Implements the [`NativeError ( message [ , options ] )`][spec] algorithm,
447    /// which is identical for `EvalError`, `RangeError`, `ReferenceError`,
448    /// `SyntaxError`, `TypeError`, and `URIError`.
449    ///
450    /// [spec]: https://tc39.es/ecma262/#sec-nativeerror
451    pub(super) fn native_error_constructor(
452        new_target: &JsValue,
453        args: &[JsValue],
454        context: &mut Context,
455        error_kind: ErrorKind,
456        constructor_fn: fn(&StandardConstructors) -> &StandardConstructor,
457    ) -> JsResult<JsValue> {
458        let new_target = &if new_target.is_undefined() {
459            context
460                .active_function_object()
461                .unwrap_or_else(|| {
462                    constructor_fn(context.intrinsics().constructors()).constructor()
463                })
464                .into()
465        } else {
466            new_target.clone()
467        };
468
469        let prototype = get_prototype_from_constructor(new_target, constructor_fn, context)?;
470        let o = JsObject::from_proto_and_data_with_shared_shape(
471            context.root_shape(),
472            prototype,
473            Error::with_caller_position(error_kind, context),
474        )
475        .upcast();
476
477        let message = args.get_or_undefined(0);
478        if !message.is_undefined() {
479            let msg = message.to_string(context)?;
480            o.create_non_enumerable_data_property_or_throw(js_string!("message"), msg, context);
481        }
482
483        Error::install_error_cause(&o, args.get_or_undefined(1), context)?;
484
485        Ok(o.into())
486    }
487}