Skip to content

Commit 0a9b2c1

Browse files
authored
Implement Error.prototype.stack accessor property (#4552)
This implements the `Error.prototype.stack` property as specified in the TC39 Error Stacks proposal (https://tc39.es/proposal-error-stacks).
1 parent 308bfda commit 0a9b2c1

File tree

6 files changed

+283
-162
lines changed

6 files changed

+283
-162
lines changed

core/engine/src/builtins/error/mod.rs

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//! [spec]: https://tc39.es/ecma262/#sec-error-objects
1111
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
1212
13+
use std::fmt::Write;
14+
1315
use crate::{
1416
Context, JsArgs, JsData, JsResult, JsString, JsValue,
1517
builtins::BuiltInObject,
@@ -20,7 +22,10 @@ use crate::{
2022
property::Attribute,
2123
realm::Realm,
2224
string::StaticJsStrings,
23-
vm::shadow_stack::{Backtrace, ShadowEntry},
25+
vm::{
26+
NativeSourceInfo,
27+
shadow_stack::{ErrorStack, ShadowEntry},
28+
},
2429
};
2530
use boa_gc::{Finalize, Trace};
2631
use boa_macros::js_str;
@@ -136,53 +141,67 @@ pub struct Error {
136141

137142
// The position of where the Error was created does not affect equality check.
138143
#[unsafe_ignore_trace]
139-
pub(crate) position: IgnoreEq<Option<ShadowEntry>>,
140-
141-
// The backtrace captured when this error was thrown. Stored here so it
142-
// survives the JsError → JsValue → JsError round-trip through promise
143-
// rejection. Does not affect equality checks.
144-
#[unsafe_ignore_trace]
145-
pub(crate) backtrace: IgnoreEq<Option<Backtrace>>,
144+
pub(crate) stack: IgnoreEq<ErrorStack>,
146145
}
147146

148147
impl Error {
149148
/// Create a new [`Error`].
150149
#[inline]
151150
#[must_use]
151+
#[cfg_attr(feature = "native-backtrace", track_caller)]
152152
pub fn new(tag: ErrorKind) -> Self {
153153
Self {
154154
tag,
155-
position: IgnoreEq(None),
156-
backtrace: IgnoreEq(None),
155+
stack: IgnoreEq(ErrorStack::Position(ShadowEntry::Native {
156+
function_name: None,
157+
source_info: NativeSourceInfo::caller(),
158+
})),
157159
}
158160
}
159161

160-
/// Create a new [`Error`] with the given optional [`ShadowEntry`].
161-
pub(crate) fn with_shadow_entry(tag: ErrorKind, entry: Option<ShadowEntry>) -> Self {
162+
/// Create a new [`Error`] with the given [`ErrorStack`].
163+
pub(crate) fn with_stack(tag: ErrorKind, location: ErrorStack) -> Self {
162164
Self {
163165
tag,
164-
position: IgnoreEq(entry),
165-
backtrace: IgnoreEq(None),
166+
stack: IgnoreEq(location),
166167
}
167168
}
168169

169170
/// Get the position from the last called function.
170171
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);
171174
Self {
172175
tag,
173-
position: IgnoreEq(context.vm.shadow_stack.caller_position()),
174-
backtrace: IgnoreEq(None),
176+
stack: IgnoreEq(ErrorStack::Backtrace(backtrace)),
175177
}
176178
}
177179
}
178180

179181
impl IntrinsicObject for Error {
180182
fn init(realm: &Realm) {
181-
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
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+
182195
let builder = BuiltInBuilder::from_standard_constructor::<Self>(realm)
183-
.property(js_string!("name"), Self::NAME, attribute)
184-
.property(js_string!("message"), js_string!(), attribute)
185-
.method(Self::to_string, js_string!("toString"), 0);
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+
);
186205

187206
#[cfg(feature = "experimental")]
188207
let builder = builder.static_method(Error::is_error, js_string!("isError"), 1);
@@ -201,7 +220,7 @@ impl BuiltInObject for Error {
201220

202221
impl BuiltInConstructor for Error {
203222
const CONSTRUCTOR_ARGUMENTS: usize = 1;
204-
const PROTOTYPE_STORAGE_SLOTS: usize = 3;
223+
const PROTOTYPE_STORAGE_SLOTS: usize = 5;
205224
const CONSTRUCTOR_STORAGE_SLOTS: usize = 1;
206225

207226
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
@@ -272,6 +291,77 @@ impl Error {
272291
Ok(())
273292
}
274293

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+
275365
/// `Error.prototype.toString()`
276366
///
277367
/// The `toString()` method returns a string representing the specified Error object.

0 commit comments

Comments
 (0)