diff --git a/cli/fmt_errors.rs b/cli/fmt_errors.rs index 6c4a4893afe539..b4e45502696c0a 100644 --- a/cli/fmt_errors.rs +++ b/cli/fmt_errors.rs @@ -128,9 +128,11 @@ fn format_frame(frame: &JsStackFrame) -> String { result } +#[allow(clippy::too_many_arguments)] fn format_stack( is_error: bool, message_line: &str, + cause: Option<&str>, source_line: Option<&str>, start_column: Option, end_column: Option, @@ -154,6 +156,14 @@ fn format_stack( indent = level )); } + if let Some(cause) = cause { + s.push_str(&format!( + "\n{:indent$}Caused by: {}", + "", + cause, + indent = level + )); + } s } @@ -262,12 +272,19 @@ impl fmt::Display for PrettyJsError { )]; } + let cause = self + .0 + .cause + .clone() + .map(|cause| format!("{}", PrettyJsError(*cause))); + write!( f, "{}", &format_stack( true, &self.0.message, + cause.as_deref(), self.0.source_line.as_deref(), self.0.start_column, self.0.end_column, diff --git a/cli/source_maps.rs b/cli/source_maps.rs index 74c390893d403f..c2b95095487dbb 100644 --- a/cli/source_maps.rs +++ b/cli/source_maps.rs @@ -79,8 +79,14 @@ pub fn apply_source_map( } } + let cause = js_error + .cause + .clone() + .map(|cause| Box::new(apply_source_map(&*cause, getter))); + JsError { message: js_error.message.clone(), + cause, source_line, script_resource_name, line_number, @@ -238,6 +244,7 @@ mod tests { fn apply_source_map_line() { let e = JsError { message: "TypeError: baz".to_string(), + cause: None, source_line: Some("foo".to_string()), script_resource_name: Some("foo_bar.ts".to_string()), line_number: Some(4), diff --git a/cli/tests/integration/mod.rs b/cli/tests/integration/mod.rs index 8dd50b2f3269e4..150683749ab497 100644 --- a/cli/tests/integration/mod.rs +++ b/cli/tests/integration/mod.rs @@ -464,6 +464,18 @@ fn broken_stdout() { assert!(!stderr.contains("panic")); } +itest!(error_cause { + args: "run error_cause.ts", + output: "error_cause.ts.out", + exit_code: 1, +}); + +itest!(error_cause_recursive { + args: "run error_cause_recursive.ts", + output: "error_cause_recursive.ts.out", + exit_code: 1, +}); + itest_flaky!(cafile_url_imports { args: "run --quiet --reload --cert tls/RootCA.pem cafile_url_imports.ts", output: "cafile_url_imports.ts.out", diff --git a/cli/tests/testdata/error_cause.ts b/cli/tests/testdata/error_cause.ts new file mode 100644 index 00000000000000..7ebd5a48a23a82 --- /dev/null +++ b/cli/tests/testdata/error_cause.ts @@ -0,0 +1,13 @@ +function a() { + throw new Error("foo", { cause: new Error("bar", { cause: "deno" }) }); +} + +function b() { + a(); +} + +function c() { + b(); +} + +c(); diff --git a/cli/tests/testdata/error_cause.ts.out b/cli/tests/testdata/error_cause.ts.out new file mode 100644 index 00000000000000..155ef656e2e89c --- /dev/null +++ b/cli/tests/testdata/error_cause.ts.out @@ -0,0 +1,17 @@ +[WILDCARD] +error: Uncaught Error: foo + throw new Error("foo", { cause: new Error("bar", { cause: "deno" }) }); + ^ + at a (file:///[WILDCARD]/error_cause.ts:2:9) + at b (file:///[WILDCARD]/error_cause.ts:6:3) + at c (file:///[WILDCARD]/error_cause.ts:10:3) + at file:///[WILDCARD]/error_cause.ts:13:1 +Caused by: Uncaught Error: bar + throw new Error("foo", { cause: new Error("bar", { cause: "deno" }) }); + ^ + at a (file:///[WILDCARD]/error_cause.ts:2:35) + at b (file:///[WILDCARD]/error_cause.ts:6:3) + at c (file:///[WILDCARD]/error_cause.ts:10:3) + at file:///[WILDCARD]/error_cause.ts:13:1 +Caused by: Uncaught deno +[WILDCARD] \ No newline at end of file diff --git a/cli/tests/testdata/error_cause_recursive.ts b/cli/tests/testdata/error_cause_recursive.ts new file mode 100644 index 00000000000000..a6999b1ff06996 --- /dev/null +++ b/cli/tests/testdata/error_cause_recursive.ts @@ -0,0 +1,4 @@ +const x = new Error("foo"); +const y = new Error("bar", { cause: x }); +x.cause = y; +throw y; diff --git a/cli/tests/testdata/error_cause_recursive.ts.out b/cli/tests/testdata/error_cause_recursive.ts.out new file mode 100644 index 00000000000000..8bfda02fb377af --- /dev/null +++ b/cli/tests/testdata/error_cause_recursive.ts.out @@ -0,0 +1,14 @@ +[WILDCARD] +error: Uncaught Error: bar +const y = new Error("bar", { cause: x }); + ^ + at file:///[WILDCARD]/error_cause_recursive.ts:2:11 +Caused by: Uncaught Error: foo +const x = new Error("foo"); + ^ + at file:///[WILDCARD]/error_cause_recursive.ts:1:11 +Caused by: Uncaught Error: bar +const y = new Error("bar", { cause: x }); + ^ + at file:///[WILDCARD]/error_cause_recursive.ts:2:11 +[WILDCARD] \ No newline at end of file diff --git a/core/error.rs b/core/error.rs index 332bc5c514a1f1..dd8d95d45ebf3a 100644 --- a/core/error.rs +++ b/core/error.rs @@ -2,6 +2,7 @@ use anyhow::Error; use std::borrow::Cow; +use std::collections::HashSet; use std::fmt; use std::fmt::Debug; use std::fmt::Display; @@ -92,6 +93,7 @@ pub fn get_custom_error_class(error: &Error) -> Option<&'static str> { #[derive(Debug, PartialEq, Clone)] pub struct JsError { pub message: String, + pub cause: Option>, pub source_line: Option, pub script_resource_name: Option, pub line_number: Option, @@ -173,6 +175,14 @@ impl JsError { pub fn from_v8_exception( scope: &mut v8::HandleScope, exception: v8::Local, + ) -> Self { + Self::inner_from_v8_exception(scope, exception, Default::default()) + } + + fn inner_from_v8_exception<'a>( + scope: &'a mut v8::HandleScope, + exception: v8::Local<'a, v8::Value>, + mut seen: HashSet>, ) -> Self { // Create a new HandleScope because we're creating a lot of new local // handles below. @@ -180,53 +190,72 @@ impl JsError { let msg = v8::Exception::create_message(scope, exception); - let (message, frames, stack) = if is_instance_of_error(scope, exception) { - // The exception is a JS Error object. - let exception: v8::Local = exception.try_into().unwrap(); - - let e: NativeJsError = - serde_v8::from_v8(scope, exception.into()).unwrap(); - // Get the message by formatting error.name and error.message. - let name = e.name.unwrap_or_else(|| "Error".to_string()); - let message_prop = e.message.unwrap_or_else(|| "".to_string()); - let message = if !name.is_empty() && !message_prop.is_empty() { - format!("Uncaught {}: {}", name, message_prop) - } else if !name.is_empty() { - format!("Uncaught {}", name) - } else if !message_prop.is_empty() { - format!("Uncaught {}", message_prop) + let (message, frames, stack, cause) = + if is_instance_of_error(scope, exception) { + // The exception is a JS Error object. + let exception: v8::Local = exception.try_into().unwrap(); + let cause = get_property(scope, exception, "cause"); + let e: NativeJsError = + serde_v8::from_v8(scope, exception.into()).unwrap(); + // Get the message by formatting error.name and error.message. + let name = e.name.unwrap_or_else(|| "Error".to_string()); + let message_prop = e.message.unwrap_or_else(|| "".to_string()); + let message = if !name.is_empty() && !message_prop.is_empty() { + format!("Uncaught {}: {}", name, message_prop) + } else if !name.is_empty() { + format!("Uncaught {}", name) + } else if !message_prop.is_empty() { + format!("Uncaught {}", message_prop) + } else { + "Uncaught".to_string() + }; + let cause = cause.and_then(|cause| { + if cause.is_undefined() || seen.contains(&cause) { + None + } else { + seen.insert(cause); + Some(Box::new(JsError::inner_from_v8_exception( + scope, cause, seen, + ))) + } + }); + + // Access error.stack to ensure that prepareStackTrace() has been called. + // This should populate error.__callSiteEvals. + let stack = get_property(scope, exception, "stack"); + let stack: Option> = + stack.and_then(|s| s.try_into().ok()); + let stack = stack.map(|s| s.to_rust_string_lossy(scope)); + + // Read an array of structured frames from error.__callSiteEvals. + let frames_v8 = get_property(scope, exception, "__callSiteEvals"); + // Ignore non-array values + let frames_v8: Option> = + frames_v8.and_then(|a| a.try_into().ok()); + + // Convert them into Vec + let frames: Vec = match frames_v8 { + Some(frames_v8) => { + serde_v8::from_v8(scope, frames_v8.into()).unwrap() + } + None => vec![], + }; + (message, frames, stack, cause) } else { - "Uncaught".to_string() - }; - - // Access error.stack to ensure that prepareStackTrace() has been called. - // This should populate error.__callSiteEvals. - let stack = get_property(scope, exception, "stack"); - let stack: Option> = - stack.and_then(|s| s.try_into().ok()); - let stack = stack.map(|s| s.to_rust_string_lossy(scope)); - - // Read an array of structured frames from error.__callSiteEvals. - let frames_v8 = get_property(scope, exception, "__callSiteEvals"); - // Ignore non-array values - let frames_v8: Option> = - frames_v8.and_then(|a| a.try_into().ok()); - - // Convert them into Vec - let frames: Vec = match frames_v8 { - Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(), - None => vec![], + // The exception is not a JS Error object. + // Get the message given by V8::Exception::create_message(), and provide + // empty frames. + ( + msg.get(scope).to_rust_string_lossy(scope), + vec![], + None, + None, + ) }; - (message, frames, stack) - } else { - // The exception is not a JS Error object. - // Get the message given by V8::Exception::create_message(), and provide - // empty frames. - (msg.get(scope).to_rust_string_lossy(scope), vec![], None) - }; Self { message, + cause, script_resource_name: msg .get_script_resource_name(scope) .and_then(|v| v8::Local::::try_from(v).ok())