diff --git a/BUILD.gn b/BUILD.gn index 1696c2483b3296..7c29e42583ade2 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -91,6 +91,7 @@ ts_sources = [ "js/os.ts", "js/platform.ts", "js/plugins.d.ts", + "js/promise_util.ts", "js/read_dir.ts", "js/read_file.ts", "js/read_link.ts", diff --git a/js/libdeno.ts b/js/libdeno.ts index 97269c36c9ecc1..842b0c8ade60bd 100644 --- a/js/libdeno.ts +++ b/js/libdeno.ts @@ -3,6 +3,12 @@ import { globalEval } from "./global_eval"; // The libdeno functions are moved so that users can't access them. type MessageCallback = (msg: Uint8Array) => void; +export type PromiseRejectEvent = + | "RejectWithNoHandler" + | "HandlerAddedAfterReject" + | "ResolveAfterResolved" + | "RejectAfterResolved"; + interface Libdeno { recv(cb: MessageCallback): void; @@ -20,6 +26,17 @@ interface Libdeno { ) => void ) => void; + setPromiseRejectHandler: ( + handler: ( + error: Error | string, + event: PromiseRejectEvent, + /* tslint:disable-next-line:no-any */ + promise: Promise + ) => void + ) => void; + + setPromiseErrorExaminer: (handler: () => boolean) => void; + mainSource: string; mainSourceMap: RawSourceMap; } diff --git a/js/main.ts b/js/main.ts index 24418b53e4bd30..0d33cf063ecc39 100644 --- a/js/main.ts +++ b/js/main.ts @@ -7,6 +7,7 @@ import { DenoCompiler } from "./compiler"; import { libdeno } from "./libdeno"; import { args } from "./deno"; import { sendSync, handleAsyncMsgFromRust } from "./dispatch"; +import { promiseErrorExaminer, promiseRejectHandler } from "./promise_util"; function sendStart(): msg.StartRes { const builder = new flatbuffers.Builder(); @@ -39,6 +40,8 @@ function onGlobalError( export default function denoMain() { libdeno.recv(handleAsyncMsgFromRust); libdeno.setGlobalErrorHandler(onGlobalError); + libdeno.setPromiseRejectHandler(promiseRejectHandler); + libdeno.setPromiseErrorExaminer(promiseErrorExaminer); const compiler = DenoCompiler.instance(); // First we send an empty "Start" message to let the privileged side know we diff --git a/js/promise_util.ts b/js/promise_util.ts new file mode 100644 index 00000000000000..a550bc3886e87b --- /dev/null +++ b/js/promise_util.ts @@ -0,0 +1,46 @@ +import { PromiseRejectEvent } from "./libdeno"; + +/* tslint:disable-next-line:no-any */ +const rejectMap = new Map, string>(); +// For uncaught promise rejection errors + +/* tslint:disable-next-line:no-any */ +const otherErrorMap = new Map, string>(); +// For reject after resolve / resolve after resolve errors + +export function promiseRejectHandler( + error: Error | string, + event: PromiseRejectEvent, + /* tslint:disable-next-line:no-any */ + promise: Promise +) { + switch (event) { + case "RejectWithNoHandler": + rejectMap.set(promise, (error as Error).stack || "RejectWithNoHandler"); + break; + case "HandlerAddedAfterReject": + rejectMap.delete(promise); + break; + default: + // error is string here + otherErrorMap.set(promise, `Promise warning: ${error as string}`); + } +} + +// Return true when continue, false to die on uncaught promise reject +export function promiseErrorExaminer(): boolean { + if (otherErrorMap.size > 0) { + for (const msg of otherErrorMap.values()) { + console.log(msg); + } + otherErrorMap.clear(); + } + if (rejectMap.size > 0) { + for (const msg of rejectMap.values()) { + console.log(msg); + } + rejectMap.clear(); + return false; + } + return true; +} diff --git a/libdeno/binding.cc b/libdeno/binding.cc index c5f582aaee30e0..37d358d78d304e 100644 --- a/libdeno/binding.cc +++ b/libdeno/binding.cc @@ -138,15 +138,59 @@ void HandleException(v8::Local context, } } -void ExitOnPromiseRejectCallback( - v8::PromiseRejectMessage promise_reject_message) { +const char* PromiseRejectStr(enum v8::PromiseRejectEvent e) { + switch (e) { + case v8::PromiseRejectEvent::kPromiseRejectWithNoHandler: + return "RejectWithNoHandler"; + case v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject: + return "HandlerAddedAfterReject"; + case v8::PromiseRejectEvent::kPromiseResolveAfterResolved: + return "ResolveAfterResolved"; + case v8::PromiseRejectEvent::kPromiseRejectAfterResolved: + return "RejectAfterResolved"; + } +} + +void PromiseRejectCallback(v8::PromiseRejectMessage promise_reject_message) { auto* isolate = v8::Isolate::GetCurrent(); Deno* d = static_cast(isolate->GetData(0)); DCHECK_EQ(d->isolate, isolate); v8::HandleScope handle_scope(d->isolate); auto exception = promise_reject_message.GetValue(); auto context = d->context.Get(d->isolate); - HandleException(context, exception); + auto promise = promise_reject_message.GetPromise(); + auto event = promise_reject_message.GetEvent(); + + v8::Context::Scope context_scope(context); + auto promise_reject_handler = d->promise_reject_handler.Get(isolate); + + if (!promise_reject_handler.IsEmpty()) { + v8::Local args[3]; + args[1] = v8_str(PromiseRejectStr(event)); + args[2] = promise; + /* error, event, promise */ + if (event == v8::PromiseRejectEvent::kPromiseRejectWithNoHandler) { + d->pending_promise_events++; + // exception only valid for kPromiseRejectWithNoHandler + args[0] = exception; + } else if (event == + v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject) { + d->pending_promise_events--; // unhandled event cancelled + if (d->pending_promise_events < 0) { + d->pending_promise_events = 0; + } + // Placeholder, not actually used + args[0] = v8_str("Promise handler added"); + } else if (event == v8::PromiseRejectEvent::kPromiseResolveAfterResolved) { + d->pending_promise_events++; + args[0] = v8_str("Promise resolved after resolved"); + } else if (event == v8::PromiseRejectEvent::kPromiseRejectAfterResolved) { + d->pending_promise_events++; + args[0] = v8_str("Promise rejected after resolved"); + } + promise_reject_handler->Call(context->Global(), 3, args); + return; + } } void Print(const v8::FunctionCallbackInfo& args) { @@ -279,6 +323,48 @@ void SetGlobalErrorHandler(const v8::FunctionCallbackInfo& args) { d->global_error_handler.Reset(isolate, func); } +// Sets the promise uncaught reject handler +void SetPromiseRejectHandler(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + Deno* d = reinterpret_cast(isolate->GetData(0)); + DCHECK_EQ(d->isolate, isolate); + + v8::HandleScope handle_scope(isolate); + + if (!d->promise_reject_handler.IsEmpty()) { + isolate->ThrowException( + v8_str("libdeno.setPromiseRejectHandler already called.")); + return; + } + + v8::Local v = args[0]; + CHECK(v->IsFunction()); + v8::Local func = v8::Local::Cast(v); + + d->promise_reject_handler.Reset(isolate, func); +} + +// Sets the promise uncaught reject handler +void SetPromiseErrorExaminer(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + Deno* d = reinterpret_cast(isolate->GetData(0)); + DCHECK_EQ(d->isolate, isolate); + + v8::HandleScope handle_scope(isolate); + + if (!d->promise_error_examiner.IsEmpty()) { + isolate->ThrowException( + v8_str("libdeno.setPromiseErrorExaminer already called.")); + return; + } + + v8::Local v = args[0]; + CHECK(v->IsFunction()); + v8::Local func = v8::Local::Cast(v); + + d->promise_error_examiner.Reset(isolate, func); +} + bool ExecuteV8StringSource(v8::Local context, const char* js_filename, v8::Local source) { @@ -354,6 +440,24 @@ void InitializeContext(v8::Isolate* isolate, v8::Local context, set_global_error_handler_val) .FromJust()); + auto set_promise_reject_handler_tmpl = + v8::FunctionTemplate::New(isolate, SetPromiseRejectHandler); + auto set_promise_reject_handler_val = + set_promise_reject_handler_tmpl->GetFunction(context).ToLocalChecked(); + CHECK(deno_val + ->Set(context, deno::v8_str("setPromiseRejectHandler"), + set_promise_reject_handler_val) + .FromJust()); + + auto set_promise_error_examiner_tmpl = + v8::FunctionTemplate::New(isolate, SetPromiseErrorExaminer); + auto set_promise_error_examiner_val = + set_promise_error_examiner_tmpl->GetFunction(context).ToLocalChecked(); + CHECK(deno_val + ->Set(context, deno::v8_str("setPromiseErrorExaminer"), + set_promise_error_examiner_val) + .FromJust()); + { auto source = deno::v8_str(js_source.c_str()); CHECK( @@ -389,6 +493,7 @@ void InitializeContext(v8::Isolate* isolate, v8::Local context, } void AddIsolate(Deno* d, v8::Isolate* isolate) { + d->pending_promise_events = 0; d->next_req_id = 0; d->isolate = isolate; // Leaving this code here because it will probably be useful later on, but @@ -397,7 +502,7 @@ void AddIsolate(Deno* d, v8::Isolate* isolate) { // d->isolate->SetAbortOnUncaughtExceptionCallback(AbortOnUncaughtExceptionCallback); // d->isolate->AddMessageListener(MessageCallback2); // d->isolate->SetFatalErrorHandler(FatalErrorCallback2); - d->isolate->SetPromiseRejectCallback(deno::ExitOnPromiseRejectCallback); + d->isolate->SetPromiseRejectCallback(deno::PromiseRejectCallback); d->isolate->SetData(0, d); } @@ -490,6 +595,36 @@ int deno_respond(Deno* d, void* user_data, int32_t req_id, deno_buf buf) { return 0; } +void deno_check_promise_errors(Deno* d) { + if (d->pending_promise_events > 0) { + auto* isolate = d->isolate; + v8::Locker locker(isolate); + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handle_scope(isolate); + + auto context = d->context.Get(d->isolate); + v8::Context::Scope context_scope(context); + + v8::TryCatch try_catch(d->isolate); + auto promise_error_examiner = d->promise_error_examiner.Get(d->isolate); + if (promise_error_examiner.IsEmpty()) { + d->last_exception = + "libdeno.setPromiseErrorExaminer has not been called."; + return; + } + v8::Local args[0]; + auto result = promise_error_examiner->Call(context->Global(), 0, args); + if (try_catch.HasCaught()) { + deno::HandleException(context, try_catch.Exception()); + } + d->pending_promise_events = 0; // reset + if (!result->BooleanValue(context).FromJust()) { + // Has uncaught promise reject error, exiting... + exit(1); + } + } +} + void deno_delete(Deno* d) { d->isolate->Dispose(); delete d; diff --git a/libdeno/deno.h b/libdeno/deno.h index 5fad6a1c325d6d..7040455249547f 100644 --- a/libdeno/deno.h +++ b/libdeno/deno.h @@ -59,6 +59,8 @@ int deno_execute(Deno* d, void* user_data, const char* js_filename, // libdeno.recv() callback. Check deno_last_exception() for exception text. int deno_respond(Deno* d, void* user_data, int32_t req_id, deno_buf buf); +void deno_check_promise_errors(Deno* d); + const char* deno_last_exception(Deno* d); void deno_terminate_execution(Deno* d); diff --git a/libdeno/internal.h b/libdeno/internal.h index 08e5cc0f59525a..f8b587658ac073 100644 --- a/libdeno/internal.h +++ b/libdeno/internal.h @@ -14,6 +14,9 @@ struct deno_s { std::string last_exception; v8::Persistent recv; v8::Persistent global_error_handler; + v8::Persistent promise_reject_handler; + v8::Persistent promise_error_examiner; + int32_t pending_promise_events; v8::Persistent context; v8::Persistent async_data_map; deno_recv_cb cb; @@ -32,10 +35,16 @@ void Print(const v8::FunctionCallbackInfo& args); void Recv(const v8::FunctionCallbackInfo& args); void Send(const v8::FunctionCallbackInfo& args); void SetGlobalErrorHandler(const v8::FunctionCallbackInfo& args); +void SetPromiseRejectHandler(const v8::FunctionCallbackInfo& args); +void SetPromiseErrorExaminer(const v8::FunctionCallbackInfo& args); static intptr_t external_references[] = { - reinterpret_cast(Print), reinterpret_cast(Recv), + reinterpret_cast(Print), + reinterpret_cast(Recv), reinterpret_cast(Send), - reinterpret_cast(SetGlobalErrorHandler), 0}; + reinterpret_cast(SetGlobalErrorHandler), + reinterpret_cast(SetPromiseRejectHandler), + reinterpret_cast(SetPromiseErrorExaminer), + 0}; Deno* NewFromSnapshot(void* user_data, deno_recv_cb cb); diff --git a/libdeno/libdeno_test.cc b/libdeno/libdeno_test.cc index e46c629786dab7..f79fa70d7ac92c 100644 --- a/libdeno/libdeno_test.cc +++ b/libdeno/libdeno_test.cc @@ -193,3 +193,15 @@ TEST(LibDenoTest, DataBuf) { EXPECT_EQ(data_buf_copy.data_ptr[1], 8); deno_delete(d); } + +TEST(LibDenoTest, PromiseRejectCatchHandling) { + static int count = 0; + Deno* d = deno_new([](auto _, int req_id, auto buf, auto data_buf) { + // If no error, nothing should be sent, and count should not increment + count++; + }); + EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "PromiseRejectCatchHandling()")); + + EXPECT_EQ(count, 0); + deno_delete(d); +} diff --git a/libdeno/libdeno_test.js b/libdeno/libdeno_test.js index 1b5137f0abb0ee..1aa09a7752071a 100644 --- a/libdeno/libdeno_test.js +++ b/libdeno/libdeno_test.js @@ -133,3 +133,43 @@ global.DataBuf = () => { b[0] = 9; b[1] = 8; }; + +global.PromiseRejectCatchHandling = () => { + let count = 0; + let promiseRef = null; + // When we have an error, libdeno sends something + function assertOrSend(cond) { + if (!cond) { + libdeno.send(new Uint8Array([42])); + } + } + libdeno.setPromiseErrorExaminer(() => { + assertOrSend(count === 2); + }); + libdeno.setPromiseRejectHandler((error, event, promise) => { + count++; + if (event === "RejectWithNoHandler") { + assertOrSend(error instanceof Error); + assertOrSend(error.message === "message"); + assertOrSend(count === 1); + promiseRef = promise; + } else if (event === "HandlerAddedAfterReject") { + assertOrSend(count === 2); + assertOrSend(promiseRef === promise); + } + // Should never reach 3! + assertOrSend(count !== 3); + }); + + async function fn() { + throw new Error("message"); + } + + (async () => { + try { + await fn(); + } catch (e) { + assertOrSend(count === 2); + } + })(); +} diff --git a/src/isolate.rs b/src/isolate.rs index 40dff5ed254b3a..5a09b8855c8e88 100644 --- a/src/isolate.rs +++ b/src/isolate.rs @@ -195,6 +195,12 @@ impl Isolate { } } + fn check_promise_errors(&self) { + unsafe { + libdeno::deno_check_promise_errors(self.libdeno_isolate); + } + } + // TODO Use Park abstraction? Note at time of writing Tokio default runtime // does not have new_with_park(). pub fn event_loop(&mut self) { @@ -205,7 +211,10 @@ impl Isolate { Err(mpsc::RecvTimeoutError::Timeout) => self.timeout(), Err(e) => panic!("recv_deadline() failed: {:?}", e), } + self.check_promise_errors(); } + // Check on done + self.check_promise_errors(); } fn ntasks_increment(&mut self) { diff --git a/src/libdeno.rs b/src/libdeno.rs index 4417ac54488b2d..732c78a210887c 100644 --- a/src/libdeno.rs +++ b/src/libdeno.rs @@ -33,6 +33,7 @@ extern "C" { pub fn deno_new(cb: DenoRecvCb) -> *const isolate; pub fn deno_delete(i: *const isolate); pub fn deno_last_exception(i: *const isolate) -> *const c_char; + pub fn deno_check_promise_errors(i: *const isolate); pub fn deno_respond( i: *const isolate, user_data: *mut c_void, diff --git a/tests/018_async_catch.ts b/tests/018_async_catch.ts new file mode 100644 index 00000000000000..b073654d97ac49 --- /dev/null +++ b/tests/018_async_catch.ts @@ -0,0 +1,14 @@ +async function fn() { + throw new Error("message"); +} +async function call() { + try { + console.log("before await fn()"); + await fn(); + console.log("after await fn()"); + } catch (error) { + console.log("catch"); + } + console.log("after try-catch"); +} +call().catch(() => console.log("outer catch")); diff --git a/tests/018_async_catch.ts.out b/tests/018_async_catch.ts.out new file mode 100644 index 00000000000000..4fc219973513d4 --- /dev/null +++ b/tests/018_async_catch.ts.out @@ -0,0 +1,3 @@ +before await fn() +catch +after try-catch diff --git a/tests/async_error.ts.out b/tests/async_error.ts.out index 862cf0f963b77c..69be815a0942e7 100644 --- a/tests/async_error.ts.out +++ b/tests/async_error.ts.out @@ -1,5 +1,6 @@ hello before error +world Error: error at foo ([WILDCARD]tests/async_error.ts:4:9) at eval ([WILDCARD]tests/async_error.ts:7:1)