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}