diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index 8cdf41f400f1..30b342279ac0 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -3909,6 +3909,19 @@ bool js::IsPromiseForAsync(JSObject* promise) { MOZ_MUST_USE bool js::AsyncFunctionThrown(JSContext* cx, Handle resultPromise, HandleValue reason) { + if (resultPromise->state() != JS::PromiseState::Pending) { + // OOM after resolving promise. + // Report a warning and ignore the result. + if (!JS_ReportErrorFlagsAndNumberASCII( + cx, JSREPORT_WARNING, GetErrorMessage, nullptr, + JSMSG_UNHANDLABLE_PROMISE_REJECTION_WARNING)) { + if (cx->isExceptionPending()) { + cx->clearPendingException(); + } + } + return true; + } + return RejectPromiseInternal(cx, resultPromise, reason); } diff --git a/js/src/jit-test/tests/async/debugger-reject-after-fulfill.js b/js/src/jit-test/tests/async/debugger-reject-after-fulfill.js new file mode 100644 index 000000000000..4b3637d2350f --- /dev/null +++ b/js/src/jit-test/tests/async/debugger-reject-after-fulfill.js @@ -0,0 +1,227 @@ +// Throwing error after resolving async function's promise should not +// overwrite the promise's state or value/reason. +// This situation can happen either with debugger interaction or OOM. + +// This testcase relies on the fact that there's a breakpoint after resolving +// the async function's promise, before leaving the async function's frame. +// This function searches for the last breakpoint before leaving the frame. +// +// - `declCode` should declare an async function `f`, and the function should +// set global variable `returning` to `true` just before return +// - `callCode` should call the function `f` and make sure the function's +// execution reaches the last breakpoint +function searchLastBreakpointBeforeReturn(declCode, callCode) { + const g = newGlobal({ newCompartment: true }); + const dbg = new Debugger(g); + g.eval(declCode); + + let hit = false; + let offset = 0; + dbg.onEnterFrame = function(frame) { + if (frame.callee && frame.callee.name == "f") { + frame.onStep = () => { + if (!g.returning) { + return undefined; + } + + offset = frame.offset; + return undefined; + }; + } + }; + + g.eval(callCode); + + drainJobQueue(); + + assertEq(offset != 0, true); + + return offset; +} + +function testWithoutAwait() { + const declCode = ` + var returning = false; + async function f() { + return (returning = true, "expected"); + }; + `; + + const callCode = ` + var p = f(); + `; + + const offset = searchLastBreakpointBeforeReturn(declCode, callCode); + + const g = newGlobal({ newCompartment: true }); + const dbg = new Debugger(g); + g.eval(declCode); + + let onPromiseSettledCount = 0; + dbg.onPromiseSettled = function(promise) { + onPromiseSettledCount++; + // No promise should be rejected. + assertEq(promise.promiseState, "fulfilled"); + + // Async function's promise should have expected value. + if (onPromiseSettledCount == 1) { + assertEq(promise.promiseValue, "expected"); + } + }; + + let hitBreakpoint = false; + dbg.onEnterFrame = function(frame) { + if (frame.callee && frame.callee.name == "f") { + frame.script.setBreakpoint(offset, { + hit() { + hitBreakpoint = true; + return { throw: "unexpected" }; + } + }); + } + }; + + enableLastWarning(); + + g.eval(` + var fulfilledValue; + var rejected = false; + `); + + g.eval(callCode); + + // The execution reaches to the last breakpoint without running job queue. + assertEq(hitBreakpoint, true); + + const warn = getLastWarning(); + assertEq(warn.message, + "unhandlable error after resolving async function's promise"); + clearLastWarning(); + + // Add reaction handler after resolution. + // This handler's job will be enqueued immediately. + g.eval(` + p.then(x => { + fulfilledValue = x; + }, e => { + rejected = true; + }); + `); + + // Run the above handler. + drainJobQueue(); + + assertEq(g.fulfilledValue, "expected"); + assertEq(onPromiseSettledCount >= 1, true); +} + +function testWithAwait() { + const declCode = ` + var resolve; + var p = new Promise(r => { resolve = r }); + var returning = false; + async function f() { + await p; + return (returning = true, "expected"); + }; + `; + + const callCode = ` + var p = f(); + `; + + const resolveCode = ` + resolve("resolve"); + `; + + const offset = searchLastBreakpointBeforeReturn(declCode, + callCode + resolveCode); + + const g = newGlobal({newCompartment: true}); + const dbg = new Debugger(g); + g.eval(declCode); + + let onPromiseSettledCount = 0; + dbg.onPromiseSettled = function(promise) { + onPromiseSettledCount++; + + // No promise should be rejected. + assertEq(promise.promiseState, "fulfilled"); + + // Async function's promise should have expected value. + if (onPromiseSettledCount == 3) { + assertEq(promise.promiseValue, "expected"); + } + }; + + let hitBreakpoint = false; + dbg.onEnterFrame = function(frame) { + if (frame.callee && frame.callee.name == "f") { + frame.script.setBreakpoint(offset, { + hit() { + hitBreakpoint = true; + return { throw: "unexpected" }; + } + }); + } + }; + + enableLastWarning(); + + g.eval(` + var fulfilledValue1; + var fulfilledValue2; + var rejected = false; + `); + + g.eval(callCode); + + assertEq(getLastWarning(), null); + + // Add reaction handler before resolution. + // This handler's job will be enqueued when `p` is resolved. + g.eval(` + p.then(x => { + fulfilledValue1 = x; + }, e => { + rejected = true; + }); + `); + + g.eval(resolveCode); + + // Run the remaining part of async function, and the above handler. + drainJobQueue(); + + // The execution reaches to the last breakpoint after running job queue for + // resolving `p`. + assertEq(hitBreakpoint, true); + + const warn = getLastWarning(); + assertEq(warn.message, + "unhandlable error after resolving async function's promise"); + clearLastWarning(); + + assertEq(g.fulfilledValue1, "expected"); + assertEq(g.rejected, false); + + // Add reaction handler after resolution. + // This handler's job will be enqueued immediately. + g.eval(` + p.then(x => { + fulfilledValue2 = x; + }, e => { + rejected = true; + }); + `); + + // Run the above handler. + drainJobQueue(); + + assertEq(g.fulfilledValue2, "expected"); + assertEq(g.rejected, false); + assertEq(onPromiseSettledCount >= 3, true); +} + +testWithoutAwait(); +testWithAwait(); diff --git a/js/src/js.msg b/js/src/js.msg index 1417d84f900b..c4b7a726231f 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -644,6 +644,9 @@ MSG_DEF(JSMSG_PROMISE_ERROR_IN_WRAPPED_REJECTION_REASON,0, JSEXN_INTERNALERR, "P MSG_DEF(JSMSG_RETURN_NOT_CALLABLE, 0, JSEXN_TYPEERR, "property 'return' of iterator is not callable") MSG_DEF(JSMSG_ITERATOR_NO_THROW, 0, JSEXN_TYPEERR, "iterator does not have a 'throw' method") +// Async Function +MSG_DEF(JSMSG_UNHANDLABLE_PROMISE_REJECTION_WARNING, 0, JSEXN_WARN, "unhandlable error after resolving async function's promise") + // Async Iteration MSG_DEF(JSMSG_FOR_AWAIT_NOT_OF, 0, JSEXN_SYNTAXERR, "'for await' loop should be used with 'of'") MSG_DEF(JSMSG_NOT_AN_ASYNC_GENERATOR, 0, JSEXN_TYPEERR, "Not an async generator")