1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-28 16:20:57 -05:00

feat(ext/web): Add error events for event listener and timer errors (#14159)

- feat: Add handleable error event for even listener errors
- feat: Add handleable error event for setTimeout()/setInterval() errors
- feat: Add Deno.core.destructureError()
- feat: Add Deno.core.terminate()
- fix: Don't throw listener errors from dispatchEvent()
- fix: Use biased mode when selecting between mod_evaluate() and
  run_event_loop() results
This commit is contained in:
Nayeem Rahman 2022-04-13 10:50:57 +01:00 committed by GitHub
parent d621ce1cf0
commit 4d18f558e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 439 additions and 130 deletions

View file

@ -236,7 +236,7 @@ jobs:
~/.cargo/registry/index ~/.cargo/registry/index
~/.cargo/registry/cache ~/.cargo/registry/cache
~/.cargo/git/db ~/.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 # In main branch, always creates fresh cache
- name: Cache build output (main) - name: Cache build output (main)
@ -252,7 +252,7 @@ jobs:
!./target/*/*.zip !./target/*/*.zip
!./target/*/*.tar.gz !./target/*/*.tar.gz
key: | 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. # Restore cache from the latest 'main' branch build.
- name: Cache build output (PR) - name: Cache build output (PR)
@ -268,7 +268,7 @@ jobs:
!./target/*/*.tar.gz !./target/*/*.tar.gz
key: never_saved key: never_saved
restore-keys: | 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'. # Don't save cache after building PRs or branches other than 'main'.
- name: Skip save cache (PR) - name: Skip save cache (PR)

View file

@ -7,10 +7,15 @@
/// <reference lib="deno.webstorage" /> /// <reference lib="deno.webstorage" />
/// <reference lib="esnext" /> /// <reference lib="esnext" />
interface WindowEventMap {
"error": ErrorEvent;
}
declare class Window extends EventTarget { declare class Window extends EventTarget {
new(): Window; new(): Window;
readonly window: Window & typeof globalThis; readonly window: Window & typeof globalThis;
readonly self: Window & typeof globalThis; readonly self: Window & typeof globalThis;
onerror: ((this: Window, ev: ErrorEvent) => any) | null;
onload: ((this: Window, ev: Event) => any) | null; onload: ((this: Window, ev: Event) => any) | null;
onunload: ((this: Window, ev: Event) => any) | null; onunload: ((this: Window, ev: Event) => any) | null;
close: () => void; close: () => void;
@ -25,10 +30,38 @@ declare class Window extends EventTarget {
location: Location; location: Location;
localStorage: Storage; localStorage: Storage;
sessionStorage: Storage; sessionStorage: Storage;
addEventListener<K extends keyof WindowEventMap>(
type: K,
listener: (
this: Window,
ev: WindowEventMap[K],
) => any,
options?: boolean | AddEventListenerOptions,
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<K extends keyof WindowEventMap>(
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 window: Window & typeof globalThis;
declare var self: 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 onload: ((this: Window, ev: Event) => any) | null;
declare var onunload: ((this: Window, ev: Event) => any) | null; declare var onunload: ((this: Window, ev: Event) => any) | null;
declare var localStorage: Storage; declare var localStorage: Storage;
@ -77,10 +110,17 @@ declare function prompt(message?: string, defaultValue?: string): string | null;
* dispatchEvent(new Event('unload')); * 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( declare function addEventListener(
type: string, type: string,
callback: EventListenerOrEventListenerObject | null, listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions | undefined, options?: boolean | AddEventListenerOptions,
): void; ): void;
/** Remove a previously registered event listener from the global scope /** Remove a previously registered event listener from the global scope
@ -91,10 +131,17 @@ declare function addEventListener(
* removeEventListener('load', listener); * 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( declare function removeEventListener(
type: string, type: string,
callback: EventListenerOrEventListenerObject | null, listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions | undefined, options?: boolean | EventListenerOptions,
): void; ): void;
// TODO(nayeemrmn): Move this to `extensions/web` where its implementation is. // TODO(nayeemrmn): Move this to `extensions/web` where its implementation is.

View file

@ -2685,3 +2685,32 @@ itest!(future_check2 {
output: "future_check2.out", output: "future_check2.out",
envs: vec![("DENO_FUTURE_CHECK".to_string(), "1".to_string())], 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",
});

View file

@ -0,0 +1,6 @@
addEventListener("foo", () => {
throw new Error("bar");
});
console.log(1);
dispatchEvent(new CustomEvent("foo"));
console.log(2);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
setTimeout(() => {
throw new Error("foo");
}, 0);

View file

@ -0,0 +1,5 @@
error: Uncaught Error: foo
throw new Error("foo");
^
at [WILDCARD]/set_timeout_error.ts:2:9
at [WILDCARD]

View file

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

View file

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

View file

@ -4,5 +4,5 @@ error: Uncaught (in worker "") Error
at [WILDCARD]/workers/drop_handle_race.js:2:9 at [WILDCARD]/workers/drop_handle_race.js:2:9
at Object.action (deno:ext/web/02_timers.js:[WILDCARD]) at Object.action (deno:ext/web/02_timers.js:[WILDCARD])
at handleTimerMacrotask (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]) at Worker.#pollControl (deno:runtime/js/11_workers.js:[WILDCARD])

View file

@ -1,10 +1,10 @@
Target from self.onmessage: [object DedicatedWorkerGlobalScope] Target from self.onmessage: [object DedicatedWorkerGlobalScope]
Target from message event listener: [object DedicatedWorkerGlobalScope] Target from message event listener: [object DedicatedWorkerGlobalScope]
Arguments from self.onerror: [ Arguments from self.onerror: [
"Some error message", "Uncaught Error: Some error message",
"", "[WILDCARD]/worker_event_handlers.js",
0, 9,
0, 9,
Error: Some error message Error: Some error message
at [WILDCARD] at [WILDCARD]
] ]

View file

@ -1,3 +1,3 @@
[WILDCARD]error: Uncaught (in worker "") Module not found "file:///[WILDCARD]/workers/doesnt_exist.js". [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]) at Worker.#pollControl ([WILDCARD])

View file

@ -1,4 +1,4 @@
error: Uncaught (in worker "") Requires read access to "[WILDCARD]local_file.ts", run again with the --allow-read flag 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 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]) at Worker.#pollControl ([WILDCARD])

View file

@ -1,4 +1,4 @@
error: Uncaught (in worker "") Requires net access to "example.com", run again with the --allow-net flag error: Uncaught (in worker "") Requires net access to "example.com", run again with the --allow-net flag
at blob:null/[WILDCARD]:1:8 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]) at Worker.#pollControl ([WILDCARD])

View file

@ -1,4 +1,4 @@
error: Uncaught (in worker "") Requires read access to "[WILDCARD]local_file.ts", run again with the --allow-read flag 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 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]) at Worker.#pollControl ([WILDCARD])

View file

@ -1,4 +1,4 @@
error: Uncaught (in worker "") Requires net access to "example.com", run again with the --allow-net flag 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 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]) at Worker.#pollControl ([WILDCARD])

View file

@ -2,5 +2,5 @@ error: Uncaught (in worker "") (in promise) TypeError: Requires net access to "e
await import("https://example.com/some/file.ts"); await import("https://example.com/some/file.ts");
^ ^
at async http://localhost:4545/workers/dynamic_remote.ts:2:1 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]) at Worker.#pollControl ([WILDCARD])

View file

@ -1,4 +1,4 @@
error: Uncaught (in worker "") Requires net access to "example.com", run again with the --allow-net flag 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 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]) at Worker.#pollControl ([WILDCARD])

View file

@ -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]
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]) at Worker.#pollControl ([WILDCARD])

View file

@ -1,5 +1,5 @@
[WILDCARD]error: Uncaught (in worker "bar") Error: foo[WILDCARD] [WILDCARD]error: Uncaught (in worker "bar") Error: foo[WILDCARD]
at foo ([WILDCARD]) at foo ([WILDCARD])
at [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]) at Worker.#pollControl ([WILDCARD])

View file

@ -1,7 +1,7 @@
error: Uncaught (in worker "foo") (in promise) Error: bar error: Uncaught (in worker "foo") Error: bar
throw new Error("bar"); throw new Error("bar");
^ ^
at onmessage ([WILDCARD]/message_handler_error.ts:[WILDCARD]) at onmessage ([WILDCARD]/message_handler_error.ts:[WILDCARD])
at [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]) at Worker.#pollControl ([WILDCARD])

View file

@ -3,7 +3,7 @@
^ ^
at foo ([WILDCARD]/workers/error.ts:[WILDCARD]) at foo ([WILDCARD]/workers/error.ts:[WILDCARD])
at [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]) 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]) at Worker.#pollControl ([WILDCARD])

View file

@ -1,6 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use crate::error::is_instance_of_error; use crate::error::is_instance_of_error;
use crate::error::JsError;
use crate::modules::get_module_type_from_assertions; use crate::modules::get_module_type_from_assertions;
use crate::modules::parse_import_assertions; use crate::modules::parse_import_assertions;
use crate::modules::validate_import_assertions; use crate::modules::validate_import_assertions;
@ -102,6 +103,12 @@ pub static EXTERNAL_REFERENCES: Lazy<v8::ExternalReferences> =
v8::ExternalReference { v8::ExternalReference {
function: abort_wasm_streaming.map_fn_to(), 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_wasm_streaming_callback,
); );
set_func(scope, core_val, "abortWasmStreaming", abort_wasm_streaming); 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`. // Direct bindings on `window`.
set_func(scope, global, "queueMicrotask", queue_microtask); 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( fn create_host_object(
scope: &mut v8::HandleScope, scope: &mut v8::HandleScope,
_args: v8::FunctionCallbackArguments, _args: v8::FunctionCallbackArguments,

View file

@ -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 /// A `JsError` represents an exception coming from V8, with stack frames and
/// line numbers. The deno_cli crate defines another `JsError` type, which wraps /// line numbers. The deno_cli crate defines another `JsError` type, which wraps
/// the one defined here, that adds source map support and colorful formatting. /// 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 struct JsError {
pub message: String, pub message: String,
pub cause: Option<Box<JsError>>, pub cause: Option<Box<JsError>>,
@ -103,7 +104,7 @@ pub struct JsError {
pub stack: Option<String>, pub stack: Option<String>,
} }
#[derive(Debug, PartialEq, Clone, serde::Deserialize)] #[derive(Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct JsStackFrame { pub struct JsStackFrame {
pub type_name: Option<String>, pub type_name: Option<String>,
@ -190,7 +191,6 @@ impl JsError {
let msg = v8::Exception::create_message(scope, exception); let msg = v8::Exception::create_message(scope, exception);
let (message, frames, stack, cause) =
if is_instance_of_error(scope, exception) { if is_instance_of_error(scope, exception) {
// The exception is a JS Error object. // The exception is a JS Error object.
let exception: v8::Local<v8::Object> = exception.try_into().unwrap(); let exception: v8::Local<v8::Object> = exception.try_into().unwrap();
@ -235,24 +235,9 @@ impl JsError {
// Convert them into Vec<JsStackFrame> // Convert them into Vec<JsStackFrame>
let frames: Vec<JsStackFrame> = match frames_v8 { let frames: Vec<JsStackFrame> = match frames_v8 {
Some(frames_v8) => { Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(),
serde_v8::from_v8(scope, frames_v8.into()).unwrap()
}
None => vec![], None => vec![],
}; };
(message, frames, stack, cause)
} 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,
)
};
Self { Self {
message, message,
cause, cause,
@ -269,6 +254,22 @@ impl JsError {
frames, frames,
stack, 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 = let mut maybe_prototype =
value.to_object(scope).unwrap().get_prototype(scope); value.to_object(scope).unwrap().get_prototype(scope);
while let Some(prototype) = maybe_prototype { while let Some(prototype) = maybe_prototype {
if !prototype.is_object() {
return false;
}
if prototype.strict_equals(error_prototype) { if prototype.strict_equals(error_prototype) {
return true; return true;
} }

View file

@ -169,6 +169,10 @@ pub(crate) struct JsRuntimeState {
pub(crate) op_ctxs: Box<[OpCtx]>, pub(crate) op_ctxs: Box<[OpCtx]>,
pub(crate) shared_array_buffer_store: Option<SharedArrayBufferStore>, pub(crate) shared_array_buffer_store: Option<SharedArrayBufferStore>,
pub(crate) compiled_wasm_module_store: Option<CompiledWasmModuleStore>, pub(crate) compiled_wasm_module_store: Option<CompiledWasmModuleStore>,
/// 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<v8::Global<v8::Value>>,
waker: AtomicWaker, waker: AtomicWaker,
} }
@ -384,6 +388,7 @@ impl JsRuntime {
op_state: op_state.clone(), op_state: op_state.clone(),
op_ctxs, op_ctxs,
have_unpolled_ops: false, have_unpolled_ops: false,
explicit_terminate_exception: None,
waker: AtomicWaker::new(), waker: AtomicWaker::new(),
}))); })));
@ -1017,19 +1022,33 @@ pub(crate) fn exception_to_err_result<'s, T>(
exception: v8::Local<v8::Value>, exception: v8::Local<v8::Value>,
in_promise: bool, in_promise: bool,
) -> Result<T, Error> { ) -> Result<T, Error> {
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
let is_terminating_exception = scope.is_execution_terminating(); let is_terminating_exception = scope.is_execution_terminating();
let mut exception = exception; let mut exception = exception;
if is_terminating_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.. // exception can be created..
scope.cancel_terminate_execution(); scope.cancel_terminate_execution();
// 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. // Maybe make a new exception object.
if exception.is_null_or_undefined() { if exception.is_null_or_undefined() {
let message = v8::String::new(scope, "execution terminated").unwrap(); let message = v8::String::new(scope, "execution terminated").unwrap();
exception = v8::Exception::error(scope, message); v8::Exception::error(scope, message)
} else {
exception
} }
});
} }
let mut js_error = JsError::from_v8_exception(scope, 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 ") 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); let js_error = (state.js_error_create_fn)(js_error);
if is_terminating_exception { if is_terminating_exception {
@ -1222,7 +1238,14 @@ impl JsRuntime {
// Update status after evaluating. // Update status after evaluating.
status = module.get_status(); 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!( assert!(
status == v8::ModuleStatus::Evaluated status == v8::ModuleStatus::Evaluated
|| status == v8::ModuleStatus::Errored || status == v8::ModuleStatus::Errored

View file

@ -7,6 +7,7 @@
"use strict"; "use strict";
((window) => { ((window) => {
const core = window.Deno.core;
const webidl = window.__bootstrap.webidl; const webidl = window.__bootstrap.webidl;
const { DOMException } = window.__bootstrap.domException; const { DOMException } = window.__bootstrap.domException;
const consoleInternal = window.__bootstrap.console; const consoleInternal = window.__bootstrap.console;
@ -32,6 +33,7 @@
ObjectPrototypeIsPrototypeOf, ObjectPrototypeIsPrototypeOf,
ReflectDefineProperty, ReflectDefineProperty,
SafeArrayIterator, SafeArrayIterator,
StringPrototypeStartsWith,
Symbol, Symbol,
SymbolFor, SymbolFor,
SymbolToStringTag, SymbolToStringTag,
@ -787,7 +789,11 @@
setCurrentTarget(eventImpl, tuple.item); setCurrentTarget(eventImpl, tuple.item);
try {
innerInvokeEventListeners(eventImpl, getListeners(tuple.item)); innerInvokeEventListeners(eventImpl, getListeners(tuple.item));
} catch (error) {
reportException(error);
}
} }
function normalizeAddEventHandlerOptions( 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.Event = Event;
window.EventTarget = EventTarget; window.EventTarget = EventTarget;
window.ErrorEvent = ErrorEvent; window.ErrorEvent = ErrorEvent;
@ -1333,6 +1382,7 @@
listenerCount, listenerCount,
}; };
window.__bootstrap.event = { window.__bootstrap.event = {
reportException,
setIsTrusted, setIsTrusted,
setTarget, setTarget,
defineEventHandler, defineEventHandler,

View file

@ -21,6 +21,7 @@
TypeError, TypeError,
} = window.__bootstrap.primordials; } = window.__bootstrap.primordials;
const { webidl } = window.__bootstrap; const { webidl } = window.__bootstrap;
const { reportException } = window.__bootstrap.event;
const { assert } = window.__bootstrap.infra; const { assert } = window.__bootstrap.infra;
function opNow() { function opNow() {
@ -139,13 +140,16 @@
// 2. // 2.
// 3. // 3.
// TODO(@andreubotella): Error handling.
if (typeof callback === "function") { if (typeof callback === "function") {
try {
FunctionPrototypeCall( FunctionPrototypeCall(
callback, callback,
globalThis, globalThis,
...new SafeArrayIterator(args), ...new SafeArrayIterator(args),
); );
} catch (error) {
reportException(error);
}
} else { } else {
// TODO(@andreubotella): eval doesn't seem to have a primordial, but // TODO(@andreubotella): eval doesn't seem to have a primordial, but
// it can be redefined in the global scope. // it can be redefined in the global scope.

View file

@ -140,14 +140,16 @@
error: null, error: null,
}); });
let handled = false;
this.dispatchEvent(event); this.dispatchEvent(event);
if (event.defaultPrevented) { // Don't bubble error event to window for loader errors (`!e.fileName`).
handled = true; // 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 () => { #pollControl = async () => {
@ -165,7 +167,7 @@
} /* falls through */ } /* falls through */
case 2: { // Error case 2: { // Error
if (!this.#handleError(data)) { if (!this.#handleError(data)) {
throw new Error("Unhandled error event in child worker."); throw new Error("Unhandled error in child worker.");
} }
break; break;
} }

View file

@ -558,6 +558,7 @@ delete Object.prototype.__proto__;
eventTarget.setEventTargetData(globalThis); eventTarget.setEventTargetData(globalThis);
defineEventHandler(window, "error");
defineEventHandler(window, "load"); defineEventHandler(window, "load");
defineEventHandler(window, "unload"); defineEventHandler(window, "unload");

View file

@ -119,6 +119,7 @@ pub struct WebWorkerInternalHandle {
has_terminated: Arc<AtomicBool>, has_terminated: Arc<AtomicBool>,
terminate_waker: Arc<AtomicWaker>, terminate_waker: Arc<AtomicWaker>,
isolate_handle: v8::IsolateHandle, isolate_handle: v8::IsolateHandle,
pub name: String,
pub worker_type: WebWorkerType, pub worker_type: WebWorkerType,
} }
@ -264,6 +265,7 @@ impl WebWorkerHandle {
fn create_handles( fn create_handles(
isolate_handle: v8::IsolateHandle, isolate_handle: v8::IsolateHandle,
name: String,
worker_type: WebWorkerType, worker_type: WebWorkerType,
) -> (WebWorkerInternalHandle, SendableWebWorkerHandle) { ) -> (WebWorkerInternalHandle, SendableWebWorkerHandle) {
let (parent_port, worker_port) = create_entangled_message_port(); 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 has_terminated = Arc::new(AtomicBool::new(false));
let terminate_waker = Arc::new(AtomicWaker::new()); let terminate_waker = Arc::new(AtomicWaker::new());
let internal_handle = WebWorkerInternalHandle { let internal_handle = WebWorkerInternalHandle {
sender: ctrl_tx, name,
port: Rc::new(parent_port), port: Rc::new(parent_port),
termination_signal: termination_signal.clone(), termination_signal: termination_signal.clone(),
has_terminated: has_terminated.clone(), has_terminated: has_terminated.clone(),
terminate_waker: terminate_waker.clone(), terminate_waker: terminate_waker.clone(),
isolate_handle: isolate_handle.clone(), isolate_handle: isolate_handle.clone(),
cancel: CancelHandle::new_rc(), cancel: CancelHandle::new_rc(),
sender: ctrl_tx,
worker_type, worker_type,
}; };
let external_handle = SendableWebWorkerHandle { let external_handle = SendableWebWorkerHandle {
@ -452,7 +455,7 @@ impl WebWorker {
let (internal_handle, external_handle) = { let (internal_handle, external_handle) = {
let handle = js_runtime.v8_isolate().thread_safe_handle(); let handle = js_runtime.v8_isolate().thread_safe_handle();
let (internal_handle, external_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 op_state = js_runtime.op_state();
let mut op_state = op_state.borrow_mut(); let mut op_state = op_state.borrow_mut();
op_state.put(internal_handle.clone()); op_state.put(internal_handle.clone());

View file

@ -217,6 +217,10 @@ impl MainWorker {
async fn evaluate_module(&mut self, id: ModuleId) -> Result<(), AnyError> { async fn evaluate_module(&mut self, id: ModuleId) -> Result<(), AnyError> {
let mut receiver = self.js_runtime.mod_evaluate(id); let mut receiver = self.js_runtime.mod_evaluate(id);
tokio::select! { tokio::select! {
// Not using biased mode leads to non-determinism for relatively simple
// programs.
biased;
maybe_result = &mut receiver => { maybe_result = &mut receiver => {
debug!("received module evaluate {:#?}", maybe_result); debug!("received module evaluate {:#?}", maybe_result);
maybe_result.expect("Module evaluation result not provided.") maybe_result.expect("Module evaluation result not provided.")