1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-03 12:58:54 -05:00

feat: output cause on JS runtime errors (#13209)

This commit is contained in:
Leo Kettmeir 2021-12-29 19:34:13 +01:00 committed by GitHub
parent 42777f2541
commit 167982be9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 154 additions and 41 deletions

View file

@ -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,

View file

@ -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),

View file

@ -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",

13
cli/tests/testdata/error_cause.ts vendored Normal file
View file

@ -0,0 +1,13 @@
function a() {
throw new Error("foo", { cause: new Error("bar", { cause: "deno" }) });
}
function b() {
a();
}
function c() {
b();
}
c();

17
cli/tests/testdata/error_cause.ts.out vendored Normal file
View file

@ -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]

View file

@ -0,0 +1,4 @@
const x = new Error("foo");
const y = new Error("bar", { cause: x });
x.cause = y;
throw y;

View file

@ -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]

View file

@ -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>,
@ -173,6 +175,14 @@ impl JsError {
pub fn from_v8_exception(
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.
@ -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<v8::Object> = exception.try_into().unwrap();
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,
)))
}
});
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)
// 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()
// 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,
)
};
// 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)
} 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())