Skip to content

Commit

Permalink
fix: Format non-error exceptions (denoland#14604)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
nayeemrmn committed Jun 6, 2022
1 parent 1081659 commit e3eae66
Show file tree
Hide file tree
Showing 16 changed files with 177 additions and 27 deletions.
6 changes: 6 additions & 0 deletions cli/tests/integration/test_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
2 changes: 1 addition & 1 deletion cli/tests/testdata/error_007_any.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
throw {};
throw { foo: "bar" };
2 changes: 1 addition & 1 deletion cli/tests/testdata/error_007_any.ts.out
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[WILDCARD]error: Uncaught #<Object>
[WILDCARD]error: Uncaught { foo: "bar" }
2 changes: 1 addition & 1 deletion cli/tests/testdata/error_cause.ts.out
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ Caused by: Error: bar
at b (file:https:///[WILDCARD]/error_cause.ts:7:3)
at c (file:https:///[WILDCARD]/error_cause.ts:11:3)
at file:https:///[WILDCARD]/error_cause.ts:14:1
Caused by: deno
Caused by: "deno"
[WILDCARD]
40 changes: 40 additions & 0 deletions cli/tests/testdata/test/non_error_thrown.out
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions cli/tests/testdata/test/non_error_thrown.ts
Original file line number Diff line number Diff line change
@@ -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] };
});
5 changes: 3 additions & 2 deletions cli/tests/testdata/test/ops_sanitizer_missing_details.out
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 8 additions & 4 deletions cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions cli/tests/testdata/test/ops_sanitizer_unstable.out
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
30 changes: 30 additions & 0 deletions core/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 28 additions & 10 deletions core/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions core/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions ext/console/02_console.js
Original file line number Diff line number Diff line change
Expand Up @@ -2322,5 +2322,6 @@
inspect,
wrapConsole,
createFilteredInspectProxy,
quoteString,
};
})(this);
6 changes: 4 additions & 2 deletions runtime/js/40_testing.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}

Expand Down
19 changes: 19 additions & 0 deletions runtime/js/99_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit e3eae66

Please sign in to comment.