summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2022-06-06 19:26:57 +0100
committerGitHub <noreply@github.com>2022-06-06 20:26:57 +0200
commite3eae662f3d753141571bd132ccb199f95c745ea (patch)
treee0cdad78f409c9b221b909d0d7dfcee286e6325f
parent1081659be176a59512a7e9e3dc93e13046a26aec (diff)
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).
-rw-r--r--cli/tests/integration/test_tests.rs6
-rw-r--r--cli/tests/testdata/error_007_any.ts2
-rw-r--r--cli/tests/testdata/error_007_any.ts.out2
-rw-r--r--cli/tests/testdata/error_cause.ts.out2
-rw-r--r--cli/tests/testdata/test/non_error_thrown.out40
-rw-r--r--cli/tests/testdata/test/non_error_thrown.ts23
-rw-r--r--cli/tests/testdata/test/ops_sanitizer_missing_details.out5
-rw-r--r--cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out12
-rw-r--r--cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out10
-rw-r--r--cli/tests/testdata/test/ops_sanitizer_unstable.out6
-rw-r--r--core/bindings.rs30
-rw-r--r--core/error.rs38
-rw-r--r--core/runtime.rs2
-rw-r--r--ext/console/02_console.js1
-rw-r--r--runtime/js/40_testing.js6
-rw-r--r--runtime/js/99_main.js19
16 files changed, 177 insertions, 27 deletions
diff --git a/cli/tests/integration/test_tests.rs b/cli/tests/integration/test_tests.rs
index 3a7e71090..c63ee6e5a 100644
--- a/cli/tests/integration/test_tests.rs
+++ b/cli/tests/integration/test_tests.rs
@@ -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,
+});
diff --git a/cli/tests/testdata/error_007_any.ts b/cli/tests/testdata/error_007_any.ts
index 778886fcb..bfef1289b 100644
--- a/cli/tests/testdata/error_007_any.ts
+++ b/cli/tests/testdata/error_007_any.ts
@@ -1 +1 @@
-throw {};
+throw { foo: "bar" };
diff --git a/cli/tests/testdata/error_007_any.ts.out b/cli/tests/testdata/error_007_any.ts.out
index 45dbffd04..b93ceb1d0 100644
--- a/cli/tests/testdata/error_007_any.ts.out
+++ b/cli/tests/testdata/error_007_any.ts.out
@@ -1 +1 @@
-[WILDCARD]error: Uncaught #<Object>
+[WILDCARD]error: Uncaught { foo: "bar" }
diff --git a/cli/tests/testdata/error_cause.ts.out b/cli/tests/testdata/error_cause.ts.out
index 512ab4326..2aab020d9 100644
--- a/cli/tests/testdata/error_cause.ts.out
+++ b/cli/tests/testdata/error_cause.ts.out
@@ -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]
diff --git a/cli/tests/testdata/test/non_error_thrown.out b/cli/tests/testdata/test/non_error_thrown.out
new file mode 100644
index 000000000..922129e29
--- /dev/null
+++ b/cli/tests/testdata/test/non_error_thrown.out
@@ -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
diff --git a/cli/tests/testdata/test/non_error_thrown.ts b/cli/tests/testdata/test/non_error_thrown.ts
new file mode 100644
index 000000000..85dc8d179
--- /dev/null
+++ b/cli/tests/testdata/test/non_error_thrown.ts
@@ -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] };
+});
diff --git a/cli/tests/testdata/test/ops_sanitizer_missing_details.out b/cli/tests/testdata/test/ops_sanitizer_missing_details.out
index 58d656012..5d1eb55df 100644
--- a/cli/tests/testdata/test/ops_sanitizer_missing_details.out
+++ b/cli/tests/testdata/test/ops_sanitizer_missing_details.out
@@ -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
diff --git a/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out b/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out
index bb9229c02..ce30ea1a6 100644
--- a/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out
+++ b/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests.out
@@ -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]
diff --git a/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out b/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out
index 7b2dc168b..305f92aba 100644
--- a/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out
+++ b/cli/tests/testdata/test/ops_sanitizer_multiple_timeout_tests_no_trace.out
@@ -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
diff --git a/cli/tests/testdata/test/ops_sanitizer_unstable.out b/cli/tests/testdata/test/ops_sanitizer_unstable.out
index 8be4e4551..47ca57865 100644
--- a/cli/tests/testdata/test/ops_sanitizer_unstable.out
+++ b/cli/tests/testdata/test/ops_sanitizer_unstable.out
@@ -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]
diff --git a/core/bindings.rs b/core/bindings.rs
index 6fb0153a1..48e9a4561 100644
--- a/core/bindings.rs
+++ b/core/bindings.rs
@@ -55,6 +55,9 @@ pub static EXTERNAL_REFERENCES: Lazy<v8::ExternalReferences> =
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(),
},
v8::ExternalReference {
@@ -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,
diff --git a/core/error.rs b/core/error.rs
index 2ba053802..c3bf6e861 100644
--- a/core/error.rs
+++ b/core/error.rs
@@ -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,
diff --git a/core/runtime.rs b/core/runtime.rs
index 3578b216a..66d28226e 100644
--- a/core/runtime.rs
+++ b/core/runtime.rs
@@ -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,
diff --git a/ext/console/02_console.js b/ext/console/02_console.js
index 9b54a64a1..638047b3a 100644
--- a/ext/console/02_console.js
+++ b/ext/console/02_console.js
@@ -2322,5 +2322,6 @@
inspect,
wrapConsole,
createFilteredInspectProxy,
+ quoteString,
};
})(this);
diff --git a/runtime/js/40_testing.js b/runtime/js/40_testing.js
index 8c7d69fb0..865482042 100644
--- a/runtime/js/40_testing.js
+++ b/runtime/js/40_testing.js
@@ -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);
};
}
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index b469a29dc..0bacaf58c 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -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,