summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeo Kettmeir <crowlkats@toaxl.com>2021-12-29 19:34:13 +0100
committerGitHub <noreply@github.com>2021-12-29 19:34:13 +0100
commit167982be9e7af35e6c12ef6c40c002200bf5e0c0 (patch)
treeb064a4db5beca73e0235e9fc7b15fc624f3b13ab
parent42777f25416ca292c56587e6cc4dfe9f50bebd81 (diff)
feat: output `cause` on JS runtime errors (#13209)
-rw-r--r--cli/fmt_errors.rs17
-rw-r--r--cli/source_maps.rs7
-rw-r--r--cli/tests/integration/mod.rs12
-rw-r--r--cli/tests/testdata/error_cause.ts13
-rw-r--r--cli/tests/testdata/error_cause.ts.out17
-rw-r--r--cli/tests/testdata/error_cause_recursive.ts4
-rw-r--r--cli/tests/testdata/error_cause_recursive.ts.out14
-rw-r--r--core/error.rs113
8 files changed, 155 insertions, 42 deletions
diff --git a/cli/fmt_errors.rs b/cli/fmt_errors.rs
index 6c4a4893a..b4e455026 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<i64>,
end_column: Option<i64>,
@@ -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 74c390893..c2b950954 100644
--- a/cli/source_maps.rs
+++ b/cli/source_maps.rs
@@ -79,8 +79,14 @@ pub fn apply_source_map<G: SourceMapGetter>(
}
}
+ 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 8dd50b2f3..150683749 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 000000000..7ebd5a48a
--- /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 000000000..155ef656e
--- /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 000000000..a6999b1ff
--- /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 000000000..8bfda02fb
--- /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 332bc5c51..dd8d95d45 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<Box<JsError>>,
pub source_line: Option<String>,
pub script_resource_name: Option<String>,
pub line_number: Option<i64>,
@@ -174,59 +176,86 @@ impl JsError {
scope: &mut v8::HandleScope,
exception: v8::Local<v8::Value>,
) -> 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<v8::Local<'a, v8::Value>>,
+ ) -> Self {
// Create a new HandleScope because we're creating a lot of new local
// handles below.
let scope = &mut v8::HandleScope::new(scope);
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<v8::Object> = 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<v8::Object> = 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<v8::Local<v8::String>> =
+ 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<v8::Local<v8::Array>> =
+ frames_v8.and_then(|a| a.try_into().ok());
+
+ // Convert them into Vec<JsStackFrame>
+ let frames: Vec<JsStackFrame> = 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<v8::Local<v8::String>> =
- 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<v8::Local<v8::Array>> =
- frames_v8.and_then(|a| a.try_into().ok());
-
- // Convert them into Vec<JsStackFrame>
- let frames: Vec<JsStackFrame> = 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::<v8::String>::try_from(v).ok())