0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

fix: Format non-error exceptions (#14604)

This commit adds "Deno.core.setFormatExceptionCallback" which
can be used to provide custom formatting for errors. It is useful
in cases when user throws something that is non-Error (eg.
a string, plain object, etc).
This commit is contained in:
Nayeem Rahman 2022-06-06 19:26:57 +01:00 committed by GitHub
parent 1081659be1
commit e3eae662f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 177 additions and 27 deletions

View file

@ -387,3 +387,9 @@ itest!(check_local_by_default2 {
http_server: true,
exit_code: 1,
});
itest!(non_error_thrown {
args: "test --quiet test/non_error_thrown.ts",
output: "test/non_error_thrown.out",
exit_code: 1,
});

View file

@ -1 +1 @@
throw {};
throw { foo: "bar" };

View file

@ -1 +1 @@
[WILDCARD]error: Uncaught #<Object>
[WILDCARD]error: Uncaught { foo: "bar" }

View file

@ -11,5 +11,5 @@ Caused by: Error: bar
at b (file:///[WILDCARD]/error_cause.ts:7:3)
at c (file:///[WILDCARD]/error_cause.ts:11:3)
at file:///[WILDCARD]/error_cause.ts:14:1
Caused by: deno
Caused by: "deno"
[WILDCARD]

View file

@ -0,0 +1,40 @@
running 6 tests from [WILDCARD]/non_error_thrown.ts
foo ... FAILED ([WILDCARD])
bar ... FAILED ([WILDCARD])
baz ... FAILED ([WILDCARD])
qux ... FAILED ([WILDCARD])
quux ... FAILED ([WILDCARD])
quuz ... FAILED ([WILDCARD])
ERRORS
foo => [WILDCARD]/non_error_thrown.ts:1:6
error: undefined
bar => [WILDCARD]/non_error_thrown.ts:5:6
error: null
baz => [WILDCARD]/non_error_thrown.ts:9:6
error: 123
qux => [WILDCARD]/non_error_thrown.ts:13:6
error: "Hello, world!"
quux => [WILDCARD]/non_error_thrown.ts:17:6
error: [ 1, 2, 3 ]
quuz => [WILDCARD]/non_error_thrown.ts:21:6
error: { a: "Hello, world!", b: [ 1, 2, 3 ] }
FAILURES
foo => [WILDCARD]/non_error_thrown.ts:1:6
bar => [WILDCARD]/non_error_thrown.ts:5:6
baz => [WILDCARD]/non_error_thrown.ts:9:6
qux => [WILDCARD]/non_error_thrown.ts:13:6
quux => [WILDCARD]/non_error_thrown.ts:17:6
quuz => [WILDCARD]/non_error_thrown.ts:21:6
test result: FAILED. 0 passed; 6 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD])
error: Test failed

View file

@ -0,0 +1,23 @@
Deno.test("foo", () => {
throw undefined;
});
Deno.test("bar", () => {
throw null;
});
Deno.test("baz", () => {
throw 123;
});
Deno.test("qux", () => {
throw "Hello, world!";
});
Deno.test("quux", () => {
throw [1, 2, 3];
});
Deno.test("quuz", () => {
throw { a: "Hello, world!", b: [1, 2, 3] };
});

View file

@ -5,11 +5,12 @@ test 1 ... FAILED [WILDCARD]
ERRORS
test 1 => ./test/ops_sanitizer_missing_details.ts:[WILDCARD]
error: Test case is leaking async ops.
error: AssertionError: Test case is leaking async ops.
- 1 async operation to op_write was started in this test, but never completed.
- 1 async operation to op_write was started in this test, but never completed.
To get more details where ops were leaked, run again with --trace-ops flag.
at [WILDCARD]
FAILURES

View file

@ -6,9 +6,9 @@ test 2 ... FAILED ([WILDCARD])
ERRORS
test 1 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD]
error: Test case is leaking async ops.
error: AssertionError: Test case is leaking async ops.
- 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operations were started here:
- 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operations were started here:
at [WILDCARD]
at setTimeout ([WILDCARD])
at test ([WILDCARD]/testdata/test/ops_sanitizer_multiple_timeout_tests.ts:4:3)
@ -21,10 +21,12 @@ error: Test case is leaking async ops.
at [WILDCARD]/testdata/test/ops_sanitizer_multiple_timeout_tests.ts:8:27
at [WILDCARD]
at [WILDCARD]
test 2 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD]
error: Test case is leaking async ops.
error: AssertionError: Test case is leaking async ops.
- 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operations were started here:
- 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operations were started here:
at [WILDCARD]
at setTimeout ([WILDCARD])
at test ([WILDCARD]/testdata/test/ops_sanitizer_multiple_timeout_tests.ts:4:3)
@ -37,6 +39,8 @@ error: Test case is leaking async ops.
at [WILDCARD]/testdata/test/ops_sanitizer_multiple_timeout_tests.ts:10:27
at [WILDCARD]
at [WILDCARD]
FAILURES
test 1 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD]

View file

@ -6,18 +6,20 @@ test 2 ... FAILED ([WILDCARD])
ERRORS
test 1 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD]
error: Test case is leaking async ops.
error: AssertionError: Test case is leaking async ops.
- 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call.
- 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call.
To get more details where ops were leaked, run again with --trace-ops flag.
at [WILDCARD]
test 2 => ./test/ops_sanitizer_multiple_timeout_tests.ts:[WILDCARD]
error: Test case is leaking async ops.
error: AssertionError: Test case is leaking async ops.
- 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call.
- 2 async operations to sleep for a duration were started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call.
To get more details where ops were leaked, run again with --trace-ops flag.
at [WILDCARD]
FAILURES

View file

@ -6,14 +6,16 @@ leak interval ... FAILED ([WILDCARD])
ERRORS
leak interval => ./test/ops_sanitizer_unstable.ts:[WILDCARD]
error: Test case is leaking async ops.
error: AssertionError: Test case is leaking async ops.
- 1 async operation to sleep for a duration was started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operation was started here:
- 1 async operation to sleep for a duration was started in this test, but never completed. This is often caused by not cancelling a `setTimeout` or `setInterval` call. The operation was started here:
at [WILDCARD]
at setInterval ([WILDCARD])
at [WILDCARD]/testdata/test/ops_sanitizer_unstable.ts:3:3
at [WILDCARD]
at [WILDCARD]
FAILURES
leak interval => ./test/ops_sanitizer_unstable.ts:[WILDCARD]

View file

@ -54,6 +54,9 @@ pub static EXTERNAL_REFERENCES: Lazy<v8::ExternalReferences> =
v8::ExternalReference {
function: set_uncaught_exception_callback.map_fn_to(),
},
v8::ExternalReference {
function: set_format_exception_callback.map_fn_to(),
},
v8::ExternalReference {
function: run_microtasks.map_fn_to(),
},
@ -214,6 +217,12 @@ pub fn initialize_context<'s>(
"setUncaughtExceptionCallback",
set_uncaught_exception_callback,
);
set_func(
scope,
core_val,
"setFormatExceptionCallback",
set_format_exception_callback,
);
set_func(scope, core_val, "runMicrotasks", run_microtasks);
set_func(scope, core_val, "hasTickScheduled", has_tick_scheduled);
set_func(
@ -646,6 +655,27 @@ fn set_uncaught_exception_callback(
}
}
/// Set a callback which formats exception messages as stored in
/// `JsError::exception_message`. The callback is passed the error value and
/// should return a string or `null`. If no callback is set or the callback
/// returns `null`, the built-in default formatting will be used.
fn set_format_exception_callback(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue,
) {
if let Ok(new) = arg0_to_cb(scope, args) {
if let Some(old) = JsRuntime::state(scope)
.borrow_mut()
.js_format_exception_cb
.replace(new)
{
let old = v8::Local::new(scope, old);
rv.set(old.into());
}
}
}
fn arg0_to_cb(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,

View file

@ -191,6 +191,20 @@ impl JsError {
let msg = v8::Exception::create_message(scope, exception);
let mut exception_message = None;
let state_rc = JsRuntime::state(scope);
let state = state_rc.borrow();
if let Some(format_exception_cb) = &state.js_format_exception_cb {
let format_exception_cb = format_exception_cb.open(scope);
let this = v8::undefined(scope).into();
let formatted = format_exception_cb.call(scope, this, &[exception]);
if let Some(formatted) = formatted {
if formatted.is_string() {
exception_message = Some(formatted.to_rust_string_lossy(scope));
}
}
}
if is_instance_of_error(scope, exception) {
// The exception is a JS Error object.
let exception: v8::Local<v8::Object> = exception.try_into().unwrap();
@ -200,15 +214,17 @@ impl JsError {
// Get the message by formatting error.name and error.message.
let name = e.name.clone().unwrap_or_else(|| "Error".to_string());
let message_prop = e.message.clone().unwrap_or_else(|| "".to_string());
let exception_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 exception_message = exception_message.unwrap_or_else(|| {
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
@ -334,13 +350,15 @@ impl JsError {
aggregated,
}
} else {
let exception_message = exception_message
.unwrap_or_else(|| msg.get(scope).to_rust_string_lossy(scope));
// The exception is not a JS Error object.
// Get the message given by V8::Exception::create_message(), and provide
// empty frames.
Self {
name: None,
message: None,
exception_message: msg.get(scope).to_rust_string_lossy(scope),
exception_message,
cause: None,
source_line: None,
source_line_frame_index: None,

View file

@ -152,6 +152,7 @@ pub(crate) struct JsRuntimeState {
pub(crate) js_nexttick_cbs: Vec<v8::Global<v8::Function>>,
pub(crate) js_promise_reject_cb: Option<v8::Global<v8::Function>>,
pub(crate) js_uncaught_exception_cb: Option<v8::Global<v8::Function>>,
pub(crate) js_format_exception_cb: Option<v8::Global<v8::Function>>,
pub(crate) has_tick_scheduled: bool,
pub(crate) js_wasm_streaming_cb: Option<v8::Global<v8::Function>>,
pub(crate) pending_promise_exceptions:
@ -386,6 +387,7 @@ impl JsRuntime {
js_nexttick_cbs: vec![],
js_promise_reject_cb: None,
js_uncaught_exception_cb: None,
js_format_exception_cb: None,
has_tick_scheduled: false,
js_wasm_streaming_cb: None,
source_map_getter: options.source_map_getter,

View file

@ -2322,5 +2322,6 @@
inspect,
wrapConsole,
createFilteredInspectProxy,
quoteString,
};
})(this);

View file

@ -217,14 +217,16 @@
let msg = `Test case is leaking async ops.
- ${ArrayPrototypeJoin(details, "\n - ")}`;
- ${ArrayPrototypeJoin(details, "\n - ")}`;
if (!core.isOpCallTracingEnabled()) {
msg +=
`\n\nTo get more details where ops were leaked, run again with --trace-ops flag.`;
} else {
msg += "\n";
}
throw msg;
throw assert(false, msg);
};
}

View file

@ -38,6 +38,8 @@ delete Object.prototype.__proto__;
const encoding = window.__bootstrap.encoding;
const colors = window.__bootstrap.colors;
const Console = window.__bootstrap.console.Console;
const inspectArgs = window.__bootstrap.console.inspectArgs;
const quoteString = window.__bootstrap.console.quoteString;
const compression = window.__bootstrap.compression;
const worker = window.__bootstrap.worker;
const internals = window.__bootstrap.internals;
@ -210,9 +212,26 @@ delete Object.prototype.__proto__;
return core.opSync("op_main_module");
}
function formatException(error) {
if (error instanceof Error) {
return null;
} else if (typeof error == "string") {
return `Uncaught ${
inspectArgs([quoteString(error)], {
colors: !colors.getNoColor(),
})
}`;
} else {
return `Uncaught ${
inspectArgs([error], { colors: !colors.getNoColor() })
}`;
}
}
function runtimeStart(runtimeOptions, source) {
core.setMacrotaskCallback(timers.handleTimerMacrotask);
core.setWasmStreamingCallback(fetch.handleWasmStreaming);
core.setFormatExceptionCallback(formatException);
version.setVersions(
runtimeOptions.denoVersion,
runtimeOptions.v8Version,