diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 483144fcfb..4faad19f86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,7 +236,7 @@ jobs: ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db - key: 5-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }} + key: 7-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }} # In main branch, always creates fresh cache - name: Cache build output (main) @@ -252,7 +252,7 @@ jobs: !./target/*/*.zip !./target/*/*.tar.gz key: | - 5-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }} + 7-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }} # Restore cache from the latest 'main' branch build. - name: Cache build output (PR) @@ -268,7 +268,7 @@ jobs: !./target/*/*.tar.gz key: never_saved restore-keys: | - 5-cargo-target-${{ matrix.os }}-${{ matrix.profile }}- + 7-cargo-target-${{ matrix.os }}-${{ matrix.profile }}- # Don't save cache after building PRs or branches other than 'main'. - name: Skip save cache (PR) diff --git a/cli/dts/lib.deno.window.d.ts b/cli/dts/lib.deno.window.d.ts index fbd0a967b6..d0e04a7a81 100644 --- a/cli/dts/lib.deno.window.d.ts +++ b/cli/dts/lib.deno.window.d.ts @@ -7,10 +7,15 @@ /// /// +interface WindowEventMap { + "error": ErrorEvent; +} + declare class Window extends EventTarget { new(): Window; readonly window: Window & typeof globalThis; readonly self: Window & typeof globalThis; + onerror: ((this: Window, ev: ErrorEvent) => any) | null; onload: ((this: Window, ev: Event) => any) | null; onunload: ((this: Window, ev: Event) => any) | null; close: () => void; @@ -25,10 +30,38 @@ declare class Window extends EventTarget { location: Location; localStorage: Storage; sessionStorage: Storage; + + addEventListener( + type: K, + listener: ( + this: Window, + ev: WindowEventMap[K], + ) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: ( + this: Window, + ev: WindowEventMap[K], + ) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; } declare var window: Window & typeof globalThis; declare var self: Window & typeof globalThis; +declare var onerror: ((this: Window, ev: ErrorEvent) => any) | null; declare var onload: ((this: Window, ev: Event) => any) | null; declare var onunload: ((this: Window, ev: Event) => any) | null; declare var localStorage: Storage; @@ -77,10 +110,17 @@ declare function prompt(message?: string, defaultValue?: string): string | null; * dispatchEvent(new Event('unload')); * ``` */ +declare function addEventListener< + K extends keyof WindowEventMap, +>( + type: K, + listener: (this: Window, ev: WindowEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, +): void; declare function addEventListener( type: string, - callback: EventListenerOrEventListenerObject | null, - options?: boolean | AddEventListenerOptions | undefined, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, ): void; /** Remove a previously registered event listener from the global scope @@ -91,10 +131,17 @@ declare function addEventListener( * removeEventListener('load', listener); * ``` */ +declare function removeEventListener< + K extends keyof WindowEventMap, +>( + type: K, + listener: (this: Window, ev: WindowEventMap[K]) => any, + options?: boolean | EventListenerOptions, +): void; declare function removeEventListener( type: string, - callback: EventListenerOrEventListenerObject | null, - options?: boolean | EventListenerOptions | undefined, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, ): void; // TODO(nayeemrmn): Move this to `extensions/web` where its implementation is. diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index a56e3f0f15..969a57a9fe 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -2685,3 +2685,32 @@ itest!(future_check2 { output: "future_check2.out", envs: vec![("DENO_FUTURE_CHECK".to_string(), "1".to_string())], }); + +itest!(event_listener_error { + args: "run --quiet event_listener_error.ts", + output: "event_listener_error.ts.out", + exit_code: 1, +}); + +itest!(event_listener_error_handled { + args: "run --quiet event_listener_error_handled.ts", + output: "event_listener_error_handled.ts.out", +}); + +// https://github.com/denoland/deno/pull/14159#issuecomment-1092285446 +itest!(event_listener_error_immediate_exit { + args: "run --quiet event_listener_error_immediate_exit.ts", + output: "event_listener_error_immediate_exit.ts.out", + exit_code: 1, +}); + +itest!(set_timeout_error { + args: "run --quiet set_timeout_error.ts", + output: "set_timeout_error.ts.out", + exit_code: 1, +}); + +itest!(set_timeout_error_handled { + args: "run --quiet set_timeout_error_handled.ts", + output: "set_timeout_error_handled.ts.out", +}); diff --git a/cli/tests/testdata/event_listener_error.ts b/cli/tests/testdata/event_listener_error.ts new file mode 100644 index 0000000000..1cbdf7bc2e --- /dev/null +++ b/cli/tests/testdata/event_listener_error.ts @@ -0,0 +1,6 @@ +addEventListener("foo", () => { + throw new Error("bar"); +}); +console.log(1); +dispatchEvent(new CustomEvent("foo")); +console.log(2); diff --git a/cli/tests/testdata/event_listener_error.ts.out b/cli/tests/testdata/event_listener_error.ts.out new file mode 100644 index 0000000000..a20a91dfdf --- /dev/null +++ b/cli/tests/testdata/event_listener_error.ts.out @@ -0,0 +1,7 @@ +1 +error: Uncaught Error: bar + throw new Error("bar"); + ^ + at [WILDCARD]/event_listener_error.ts:2:9 + at [WILDCARD] + at [WILDCARD]/event_listener_error.ts:5:1 diff --git a/cli/tests/testdata/event_listener_error_handled.ts b/cli/tests/testdata/event_listener_error_handled.ts new file mode 100644 index 0000000000..c4c8fd1cd9 --- /dev/null +++ b/cli/tests/testdata/event_listener_error_handled.ts @@ -0,0 +1,23 @@ +addEventListener("error", (event) => { + console.log({ + cancelable: event.cancelable, + message: event.message, + filename: event.filename?.slice?.(-100), + lineno: event.lineno, + colno: event.colno, + error: event.error, + }); + event.preventDefault(); +}); + +onerror = (event) => { + console.log("onerror() called", event.error); +}; + +addEventListener("foo", () => { + throw new Error("bar"); +}); + +console.log(1); +dispatchEvent(new CustomEvent("foo")); +console.log(2); diff --git a/cli/tests/testdata/event_listener_error_handled.ts.out b/cli/tests/testdata/event_listener_error_handled.ts.out new file mode 100644 index 0000000000..d3cf525c33 --- /dev/null +++ b/cli/tests/testdata/event_listener_error_handled.ts.out @@ -0,0 +1,17 @@ +1 +{ + cancelable: true, + message: "Uncaught Error: bar", + filename: "[WILDCARD]/event_listener_error_handled.ts", + lineno: 18, + colno: 9, + error: Error: bar + at [WILDCARD]/event_listener_error_handled.ts:18:9 + at [WILDCARD] + at [WILDCARD]/event_listener_error_handled.ts:22:1 +} +onerror() called Error: bar + at [WILDCARD]/event_listener_error_handled.ts:18:9 + at [WILDCARD] + at [WILDCARD]/event_listener_error_handled.ts:22:1 +2 diff --git a/cli/tests/testdata/event_listener_error_immediate_exit.ts b/cli/tests/testdata/event_listener_error_immediate_exit.ts new file mode 100644 index 0000000000..c9e94c01bd --- /dev/null +++ b/cli/tests/testdata/event_listener_error_immediate_exit.ts @@ -0,0 +1,12 @@ +addEventListener("foo", () => { + queueMicrotask(() => console.log("queueMicrotask")); + setTimeout(() => console.log("timer"), 0); + throw new Error("bar"); +}); +console.log(1); +// @ts-ignore Deno.core +Deno.core.setNextTickCallback(() => console.log("nextTick")); +// @ts-ignore Deno.core +Deno.core.setHasTickScheduled(true); +dispatchEvent(new CustomEvent("foo")); +console.log(2); diff --git a/cli/tests/testdata/event_listener_error_immediate_exit.ts.out b/cli/tests/testdata/event_listener_error_immediate_exit.ts.out new file mode 100644 index 0000000000..8f03f71b81 --- /dev/null +++ b/cli/tests/testdata/event_listener_error_immediate_exit.ts.out @@ -0,0 +1,6 @@ +1 +error: Uncaught Error: bar + throw new Error("bar"); + ^ + at [WILDCARD]/event_listener_error_immediate_exit.ts:4:9[WILDCARD] + at [WILDCARD]/event_listener_error_immediate_exit.ts:11:1 diff --git a/cli/tests/testdata/set_timeout_error.ts b/cli/tests/testdata/set_timeout_error.ts new file mode 100644 index 0000000000..2864574e7b --- /dev/null +++ b/cli/tests/testdata/set_timeout_error.ts @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error("foo"); +}, 0); diff --git a/cli/tests/testdata/set_timeout_error.ts.out b/cli/tests/testdata/set_timeout_error.ts.out new file mode 100644 index 0000000000..9db053f6c1 --- /dev/null +++ b/cli/tests/testdata/set_timeout_error.ts.out @@ -0,0 +1,5 @@ +error: Uncaught Error: foo + throw new Error("foo"); + ^ + at [WILDCARD]/set_timeout_error.ts:2:9 + at [WILDCARD] diff --git a/cli/tests/testdata/set_timeout_error_handled.ts b/cli/tests/testdata/set_timeout_error_handled.ts new file mode 100644 index 0000000000..aee2d97d22 --- /dev/null +++ b/cli/tests/testdata/set_timeout_error_handled.ts @@ -0,0 +1,19 @@ +addEventListener("error", (event) => { + console.log({ + cancelable: event.cancelable, + message: event.message, + filename: event.filename?.slice?.(-100), + lineno: event.lineno, + colno: event.colno, + error: event.error, + }); + event.preventDefault(); +}); + +onerror = (event) => { + console.log("onerror() called", event.error); +}; + +setTimeout(() => { + throw new Error("foo"); +}, 0); diff --git a/cli/tests/testdata/set_timeout_error_handled.ts.out b/cli/tests/testdata/set_timeout_error_handled.ts.out new file mode 100644 index 0000000000..054dd9b6b8 --- /dev/null +++ b/cli/tests/testdata/set_timeout_error_handled.ts.out @@ -0,0 +1,13 @@ +{ + cancelable: true, + message: "Uncaught Error: foo", + filename: "[WILDCARD]/set_timeout_error_handled.ts", + lineno: 18, + colno: 9, + error: Error: foo + at [WILDCARD]/set_timeout_error_handled.ts:18:9 + at [WILDCARD] +} +onerror() called Error: foo + at [WILDCARD]/set_timeout_error_handled.ts:18:9 + at [WILDCARD] diff --git a/cli/tests/testdata/worker_drop_handle_race.js.out b/cli/tests/testdata/worker_drop_handle_race.js.out index 34c2d5be26..a81684bfa7 100644 --- a/cli/tests/testdata/worker_drop_handle_race.js.out +++ b/cli/tests/testdata/worker_drop_handle_race.js.out @@ -4,5 +4,5 @@ error: Uncaught (in worker "") Error at [WILDCARD]/workers/drop_handle_race.js:2:9 at Object.action (deno:ext/web/02_timers.js:[WILDCARD]) at handleTimerMacrotask (deno:ext/web/02_timers.js:[WILDCARD]) -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl (deno:runtime/js/11_workers.js:[WILDCARD]) diff --git a/cli/tests/testdata/worker_event_handler_test.js.out b/cli/tests/testdata/worker_event_handler_test.js.out index 5556633b14..b3eed7f6c8 100644 --- a/cli/tests/testdata/worker_event_handler_test.js.out +++ b/cli/tests/testdata/worker_event_handler_test.js.out @@ -1,10 +1,10 @@ Target from self.onmessage: [object DedicatedWorkerGlobalScope] Target from message event listener: [object DedicatedWorkerGlobalScope] Arguments from self.onerror: [ - "Some error message", - "", - 0, - 0, + "Uncaught Error: Some error message", + "[WILDCARD]/worker_event_handlers.js", + 9, + 9, Error: Some error message at [WILDCARD] ] diff --git a/cli/tests/testdata/workers/nonexistent_worker.out b/cli/tests/testdata/workers/nonexistent_worker.out index 1b5111b14c..08a7e74e70 100644 --- a/cli/tests/testdata/workers/nonexistent_worker.out +++ b/cli/tests/testdata/workers/nonexistent_worker.out @@ -1,3 +1,3 @@ [WILDCARD]error: Uncaught (in worker "") Module not found "file:///[WILDCARD]/workers/doesnt_exist.js". -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/permissions_blob_local.ts.out b/cli/tests/testdata/workers/permissions_blob_local.ts.out index ee19c7ab56..9d5336bfe8 100644 --- a/cli/tests/testdata/workers/permissions_blob_local.ts.out +++ b/cli/tests/testdata/workers/permissions_blob_local.ts.out @@ -1,4 +1,4 @@ error: Uncaught (in worker "") Requires read access to "[WILDCARD]local_file.ts", run again with the --allow-read flag at blob:null/[WILDCARD]:1:8 -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/permissions_blob_remote.ts.out b/cli/tests/testdata/workers/permissions_blob_remote.ts.out index 597e5bf1e8..ac06e6ccb4 100644 --- a/cli/tests/testdata/workers/permissions_blob_remote.ts.out +++ b/cli/tests/testdata/workers/permissions_blob_remote.ts.out @@ -1,4 +1,4 @@ error: Uncaught (in worker "") Requires net access to "example.com", run again with the --allow-net flag at blob:null/[WILDCARD]:1:8 -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/permissions_data_local.ts.out b/cli/tests/testdata/workers/permissions_data_local.ts.out index 5c9bcf1c14..96711f9a08 100644 --- a/cli/tests/testdata/workers/permissions_data_local.ts.out +++ b/cli/tests/testdata/workers/permissions_data_local.ts.out @@ -1,4 +1,4 @@ error: Uncaught (in worker "") Requires read access to "[WILDCARD]local_file.ts", run again with the --allow-read flag at data:application/javascript;base64,[WILDCARD]:1:8 -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/permissions_data_remote.ts.out b/cli/tests/testdata/workers/permissions_data_remote.ts.out index 0273664bdc..a9ed5c2401 100644 --- a/cli/tests/testdata/workers/permissions_data_remote.ts.out +++ b/cli/tests/testdata/workers/permissions_data_remote.ts.out @@ -1,4 +1,4 @@ error: Uncaught (in worker "") Requires net access to "example.com", run again with the --allow-net flag at data:application/javascript;base64,aW1wb3J0ICJodHRwczovL2V4YW1wbGUuY29tL3NvbWUvZmlsZS50cyI7:1:8 -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/permissions_dynamic_remote.ts.out b/cli/tests/testdata/workers/permissions_dynamic_remote.ts.out index e5015abff4..cbddb61e04 100644 --- a/cli/tests/testdata/workers/permissions_dynamic_remote.ts.out +++ b/cli/tests/testdata/workers/permissions_dynamic_remote.ts.out @@ -2,5 +2,5 @@ error: Uncaught (in worker "") (in promise) TypeError: Requires net access to "e await import("https://example.com/some/file.ts"); ^ at async http://localhost:4545/workers/dynamic_remote.ts:2:1 -[WILDCARD]error: Uncaught (in promise) Error: Unhandled error event in child worker. +[WILDCARD]error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/permissions_remote_remote.ts.out b/cli/tests/testdata/workers/permissions_remote_remote.ts.out index 42602cf715..be96b5d4ee 100644 --- a/cli/tests/testdata/workers/permissions_remote_remote.ts.out +++ b/cli/tests/testdata/workers/permissions_remote_remote.ts.out @@ -1,4 +1,4 @@ error: Uncaught (in worker "") Requires net access to "example.com", run again with the --allow-net flag at http://localhost:4545/workers/static_remote.ts:2:8 -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/worker_async_error.ts.out b/cli/tests/testdata/workers/worker_async_error.ts.out index 0a05534c5f..16f6a6b84e 100644 --- a/cli/tests/testdata/workers/worker_async_error.ts.out +++ b/cli/tests/testdata/workers/worker_async_error.ts.out @@ -3,5 +3,5 @@ error: Uncaught (in worker "foo") (in promise) Error: bar ^ at [WILDCARD]/async_error.ts:[WILDCARD] at [WILDCARD]/async_error.ts:[WILDCARD] -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/worker_error.ts.out b/cli/tests/testdata/workers/worker_error.ts.out index cb0a025507..e7b0ea14d2 100644 --- a/cli/tests/testdata/workers/worker_error.ts.out +++ b/cli/tests/testdata/workers/worker_error.ts.out @@ -1,5 +1,5 @@ [WILDCARD]error: Uncaught (in worker "bar") Error: foo[WILDCARD] at foo ([WILDCARD]) at [WILDCARD] -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/worker_message_handler_error.ts.out b/cli/tests/testdata/workers/worker_message_handler_error.ts.out index 56458d5e44..0c51ca9d23 100644 --- a/cli/tests/testdata/workers/worker_message_handler_error.ts.out +++ b/cli/tests/testdata/workers/worker_message_handler_error.ts.out @@ -1,7 +1,7 @@ -error: Uncaught (in worker "foo") (in promise) Error: bar +error: Uncaught (in worker "foo") Error: bar throw new Error("bar"); ^ at onmessage ([WILDCARD]/message_handler_error.ts:[WILDCARD]) at [WILDCARD] -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/cli/tests/testdata/workers/worker_nested_error.ts.out b/cli/tests/testdata/workers/worker_nested_error.ts.out index 5c978ca9bb..afd9f59e2b 100644 --- a/cli/tests/testdata/workers/worker_nested_error.ts.out +++ b/cli/tests/testdata/workers/worker_nested_error.ts.out @@ -3,7 +3,7 @@ ^ at foo ([WILDCARD]/workers/error.ts:[WILDCARD]) at [WILDCARD]/workers/error.ts:[WILDCARD] -error: Uncaught (in worker "baz") (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in worker "baz") (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) -error: Uncaught (in promise) Error: Unhandled error event in child worker. +error: Uncaught (in promise) Error: Unhandled error in child worker. at Worker.#pollControl ([WILDCARD]) diff --git a/core/bindings.rs b/core/bindings.rs index 5e37232a1f..46dcefe389 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -1,6 +1,7 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use crate::error::is_instance_of_error; +use crate::error::JsError; use crate::modules::get_module_type_from_assertions; use crate::modules::parse_import_assertions; use crate::modules::validate_import_assertions; @@ -102,6 +103,12 @@ pub static EXTERNAL_REFERENCES: Lazy = v8::ExternalReference { function: abort_wasm_streaming.map_fn_to(), }, + v8::ExternalReference { + function: destructure_error.map_fn_to(), + }, + v8::ExternalReference { + function: terminate.map_fn_to(), + }, ]) }); @@ -228,6 +235,8 @@ pub fn initialize_context<'s>( set_wasm_streaming_callback, ); set_func(scope, core_val, "abortWasmStreaming", abort_wasm_streaming); + set_func(scope, core_val, "destructureError", destructure_error); + set_func(scope, core_val, "terminate", terminate); // Direct bindings on `window`. set_func(scope, global, "queueMicrotask", queue_microtask); @@ -1293,6 +1302,28 @@ fn queue_microtask( }; } +fn destructure_error( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue, +) { + let js_error = JsError::from_v8_exception(scope, args.get(0)); + let object = serde_v8::to_v8(scope, js_error).unwrap(); + rv.set(object); +} + +fn terminate( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + let state_rc = JsRuntime::state(scope); + let mut state = state_rc.borrow_mut(); + state.explicit_terminate_exception = + Some(v8::Global::new(scope, args.get(0))); + scope.terminate_execution(); +} + fn create_host_object( scope: &mut v8::HandleScope, _args: v8::FunctionCallbackArguments, diff --git a/core/error.rs b/core/error.rs index 106845f046..1fc4a1af77 100644 --- a/core/error.rs +++ b/core/error.rs @@ -90,7 +90,8 @@ pub fn get_custom_error_class(error: &Error) -> Option<&'static str> { /// A `JsError` represents an exception coming from V8, with stack frames and /// line numbers. The deno_cli crate defines another `JsError` type, which wraps /// the one defined here, that adds source map support and colorful formatting. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] pub struct JsError { pub message: String, pub cause: Option>, @@ -103,7 +104,7 @@ pub struct JsError { pub stack: Option, } -#[derive(Debug, PartialEq, Clone, serde::Deserialize)] +#[derive(Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct JsStackFrame { pub type_name: Option, @@ -190,84 +191,84 @@ impl JsError { let msg = v8::Exception::create_message(scope, exception); - let (message, frames, stack, cause) = - if is_instance_of_error(scope, exception) { - // The exception is a JS Error object. - let exception: v8::Local = 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> = - 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> = - frames_v8.and_then(|a| a.try_into().ok()); - - // Convert them into Vec - let frames: Vec = match frames_v8 { - Some(frames_v8) => { - serde_v8::from_v8(scope, frames_v8.into()).unwrap() - } - None => vec![], - }; - (message, frames, stack, cause) + if is_instance_of_error(scope, exception) { + // The exception is a JS Error object. + let exception: v8::Local = 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 { - // 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, - ) + "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, + ))) + } + }); - Self { - message, - cause, - script_resource_name: msg - .get_script_resource_name(scope) - .and_then(|v| v8::Local::::try_from(v).ok()) - .map(|v| v.to_rust_string_lossy(scope)), - source_line: msg - .get_source_line(scope) - .map(|v| v.to_rust_string_lossy(scope)), - line_number: msg.get_line_number(scope).and_then(|v| v.try_into().ok()), - start_column: msg.get_start_column().try_into().ok(), - end_column: msg.get_end_column().try_into().ok(), - frames, - stack, + // 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> = + 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> = + frames_v8.and_then(|a| a.try_into().ok()); + + // Convert them into Vec + let frames: Vec = match frames_v8 { + Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(), + None => vec![], + }; + Self { + message, + cause, + script_resource_name: msg + .get_script_resource_name(scope) + .and_then(|v| v8::Local::::try_from(v).ok()) + .map(|v| v.to_rust_string_lossy(scope)), + source_line: msg + .get_source_line(scope) + .map(|v| v.to_rust_string_lossy(scope)), + line_number: msg.get_line_number(scope).and_then(|v| v.try_into().ok()), + start_column: msg.get_start_column().try_into().ok(), + end_column: msg.get_end_column().try_into().ok(), + frames, + stack, + } + } else { + // The exception is not a JS Error object. + // Get the message given by V8::Exception::create_message(), and provide + // empty frames. + Self { + message: msg.get(scope).to_rust_string_lossy(scope), + cause: None, + script_resource_name: None, + source_line: None, + line_number: None, + start_column: None, + end_column: None, + frames: vec![], + stack: None, + } } } } @@ -337,6 +338,9 @@ pub(crate) fn is_instance_of_error<'s>( let mut maybe_prototype = value.to_object(scope).unwrap().get_prototype(scope); while let Some(prototype) = maybe_prototype { + if !prototype.is_object() { + return false; + } if prototype.strict_equals(error_prototype) { return true; } diff --git a/core/runtime.rs b/core/runtime.rs index 50f7399d4c..7f7b6ead1f 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -169,6 +169,10 @@ pub(crate) struct JsRuntimeState { pub(crate) op_ctxs: Box<[OpCtx]>, pub(crate) shared_array_buffer_store: Option, pub(crate) compiled_wasm_module_store: Option, + /// The error that was passed to an explicit `Deno.core.terminate` call. + /// It will be retrieved by `exception_to_err_result` and used as an error + /// instead of any other exceptions. + pub(crate) explicit_terminate_exception: Option>, waker: AtomicWaker, } @@ -384,6 +388,7 @@ impl JsRuntime { op_state: op_state.clone(), op_ctxs, have_unpolled_ops: false, + explicit_terminate_exception: None, waker: AtomicWaker::new(), }))); @@ -1017,19 +1022,33 @@ pub(crate) fn exception_to_err_result<'s, T>( exception: v8::Local, in_promise: bool, ) -> Result { + let state_rc = JsRuntime::state(scope); + let mut state = state_rc.borrow_mut(); + let is_terminating_exception = scope.is_execution_terminating(); let mut exception = exception; if is_terminating_exception { - // TerminateExecution was called. Cancel exception termination so that the + // TerminateExecution was called. Cancel isolate termination so that the // exception can be created.. scope.cancel_terminate_execution(); - // Maybe make a new exception object. - if exception.is_null_or_undefined() { - let message = v8::String::new(scope, "execution terminated").unwrap(); - exception = v8::Exception::error(scope, message); - } + // If the termination is the result of a `Deno.core.terminate` call, we want + // to use the exception that was passed to it rather than the exception that + // was passed to this function. + exception = state + .explicit_terminate_exception + .take() + .map(|exception| v8::Local::new(scope, exception)) + .unwrap_or_else(|| { + // Maybe make a new exception object. + if exception.is_null_or_undefined() { + let message = v8::String::new(scope, "execution terminated").unwrap(); + v8::Exception::error(scope, message) + } else { + exception + } + }); } let mut js_error = JsError::from_v8_exception(scope, exception); @@ -1039,9 +1058,6 @@ pub(crate) fn exception_to_err_result<'s, T>( js_error.message.trim_start_matches("Uncaught ") ); } - - let state_rc = JsRuntime::state(scope); - let state = state_rc.borrow(); let js_error = (state.js_error_create_fn)(js_error); if is_terminating_exception { @@ -1222,7 +1238,14 @@ impl JsRuntime { // Update status after evaluating. status = module.get_status(); - if let Some(value) = maybe_value { + let explicit_terminate_exception = + state_rc.borrow_mut().explicit_terminate_exception.take(); + if let Some(exception) = explicit_terminate_exception { + let exception = v8::Local::new(tc_scope, exception); + sender + .send(exception_to_err_result(tc_scope, exception, false)) + .expect("Failed to send module evaluation error."); + } else if let Some(value) = maybe_value { assert!( status == v8::ModuleStatus::Evaluated || status == v8::ModuleStatus::Errored @@ -3093,8 +3116,8 @@ assertEquals(1, notify_return_value); const a1b = a1.subarray(0, 3); const a2 = new Uint8Array([5,10,15]); const a2b = a2.subarray(0, 3); - - + + if (!(a1.length > 0 && a1b.length > 0)) { throw new Error("a1 & a1b should have a length"); } @@ -3116,7 +3139,7 @@ assertEquals(1, notify_return_value); if (a2.byteLength > 0 || a2b.byteLength > 0) { throw new Error("expecting a2 & a2b to be detached, a3 re-attached"); } - + const wmem = new WebAssembly.Memory({ initial: 1, maximum: 2 }); const w32 = new Uint32Array(wmem.buffer); w32[0] = 1; w32[1] = 2; w32[2] = 3; diff --git a/ext/web/02_event.js b/ext/web/02_event.js index 122add2114..f1078b4ac2 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -7,6 +7,7 @@ "use strict"; ((window) => { + const core = window.Deno.core; const webidl = window.__bootstrap.webidl; const { DOMException } = window.__bootstrap.domException; const consoleInternal = window.__bootstrap.console; @@ -32,6 +33,7 @@ ObjectPrototypeIsPrototypeOf, ReflectDefineProperty, SafeArrayIterator, + StringPrototypeStartsWith, Symbol, SymbolFor, SymbolToStringTag, @@ -787,7 +789,11 @@ setCurrentTarget(eventImpl, tuple.item); - innerInvokeEventListeners(eventImpl, getListeners(tuple.item)); + try { + innerInvokeEventListeners(eventImpl, getListeners(tuple.item)); + } catch (error) { + reportException(error); + } } function normalizeAddEventHandlerOptions( @@ -1317,6 +1323,49 @@ }); } + let reportExceptionStackedCalls = 0; + + // https://html.spec.whatwg.org/#report-the-exception + function reportException(error) { + reportExceptionStackedCalls++; + const jsError = core.destructureError(error); + const message = jsError.message; + let filename = ""; + let lineno = 0; + let colno = 0; + if (jsError.frames.length > 0) { + filename = jsError.frames[0].fileName; + lineno = jsError.frames[0].lineNumber; + colno = jsError.frames[0].columnNumber; + } else { + const jsError = core.destructureError(new Error()); + for (const frame of jsError.frames) { + if ( + typeof frame.fileName == "string" && + !StringPrototypeStartsWith(frame.fileName, "deno:") + ) { + filename = frame.fileName; + lineno = frame.lineNumber; + colno = frame.columnNumber; + break; + } + } + } + const event = new ErrorEvent("error", { + cancelable: true, + message, + filename, + lineno, + colno, + error, + }); + // Avoid recursing `reportException()` via error handlers more than once. + if (reportExceptionStackedCalls > 1 || window.dispatchEvent(event)) { + core.terminate(error); + } + reportExceptionStackedCalls--; + } + window.Event = Event; window.EventTarget = EventTarget; window.ErrorEvent = ErrorEvent; @@ -1333,6 +1382,7 @@ listenerCount, }; window.__bootstrap.event = { + reportException, setIsTrusted, setTarget, defineEventHandler, diff --git a/ext/web/02_timers.js b/ext/web/02_timers.js index 808d995638..edad89ace4 100644 --- a/ext/web/02_timers.js +++ b/ext/web/02_timers.js @@ -21,6 +21,7 @@ TypeError, } = window.__bootstrap.primordials; const { webidl } = window.__bootstrap; + const { reportException } = window.__bootstrap.event; const { assert } = window.__bootstrap.infra; function opNow() { @@ -139,13 +140,16 @@ // 2. // 3. - // TODO(@andreubotella): Error handling. if (typeof callback === "function") { - FunctionPrototypeCall( - callback, - globalThis, - ...new SafeArrayIterator(args), - ); + try { + FunctionPrototypeCall( + callback, + globalThis, + ...new SafeArrayIterator(args), + ); + } catch (error) { + reportException(error); + } } else { // TODO(@andreubotella): eval doesn't seem to have a primordial, but // it can be redefined in the global scope. diff --git a/runtime/js/11_workers.js b/runtime/js/11_workers.js index 80e85a3a15..5b8d03e714 100644 --- a/runtime/js/11_workers.js +++ b/runtime/js/11_workers.js @@ -140,14 +140,16 @@ error: null, }); - let handled = false; - this.dispatchEvent(event); - if (event.defaultPrevented) { - handled = true; + // Don't bubble error event to window for loader errors (`!e.fileName`). + // TODO(nayeemrmn): Currently these are never bubbled because worker + // error event fields aren't populated correctly and `e.fileName` is + // always empty. + if (e.fileName && !event.defaultPrevented) { + window.dispatchEvent(event); } - return handled; + return event.defaultPrevented; } #pollControl = async () => { @@ -165,7 +167,7 @@ } /* falls through */ case 2: { // Error if (!this.#handleError(data)) { - throw new Error("Unhandled error event in child worker."); + throw new Error("Unhandled error in child worker."); } break; } diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 3a37abeab8..44d457e5cb 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -558,6 +558,7 @@ delete Object.prototype.__proto__; eventTarget.setEventTargetData(globalThis); + defineEventHandler(window, "error"); defineEventHandler(window, "load"); defineEventHandler(window, "unload"); diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 0e31670419..014b3e738b 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -119,6 +119,7 @@ pub struct WebWorkerInternalHandle { has_terminated: Arc, terminate_waker: Arc, isolate_handle: v8::IsolateHandle, + pub name: String, pub worker_type: WebWorkerType, } @@ -264,6 +265,7 @@ impl WebWorkerHandle { fn create_handles( isolate_handle: v8::IsolateHandle, + name: String, worker_type: WebWorkerType, ) -> (WebWorkerInternalHandle, SendableWebWorkerHandle) { let (parent_port, worker_port) = create_entangled_message_port(); @@ -272,13 +274,14 @@ fn create_handles( let has_terminated = Arc::new(AtomicBool::new(false)); let terminate_waker = Arc::new(AtomicWaker::new()); let internal_handle = WebWorkerInternalHandle { - sender: ctrl_tx, + name, port: Rc::new(parent_port), termination_signal: termination_signal.clone(), has_terminated: has_terminated.clone(), terminate_waker: terminate_waker.clone(), isolate_handle: isolate_handle.clone(), cancel: CancelHandle::new_rc(), + sender: ctrl_tx, worker_type, }; let external_handle = SendableWebWorkerHandle { @@ -452,7 +455,7 @@ impl WebWorker { let (internal_handle, external_handle) = { let handle = js_runtime.v8_isolate().thread_safe_handle(); let (internal_handle, external_handle) = - create_handles(handle, options.worker_type); + create_handles(handle, name.clone(), options.worker_type); let op_state = js_runtime.op_state(); let mut op_state = op_state.borrow_mut(); op_state.put(internal_handle.clone()); diff --git a/runtime/worker.rs b/runtime/worker.rs index db9490dbed..983f174aa7 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -217,6 +217,10 @@ impl MainWorker { async fn evaluate_module(&mut self, id: ModuleId) -> Result<(), AnyError> { let mut receiver = self.js_runtime.mod_evaluate(id); tokio::select! { + // Not using biased mode leads to non-determinism for relatively simple + // programs. + biased; + maybe_result = &mut receiver => { debug!("received module evaluate {:#?}", maybe_result); maybe_result.expect("Module evaluation result not provided.")