diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index 6be945d8c5..c288c3a830 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -524,7 +524,7 @@ void VM::run_queued_promise_jobs() dbgln_if(PROMISE_DEBUG, "Running queued promise jobs"); // Temporarily get rid of the exception, if any - job functions must be called // either way, and that can't happen if we already have an exception stored. - TemporaryClearException clear_exception(*this); + TemporaryClearException temporary_clear_exception(*this); while (!m_promise_jobs.is_empty()) { auto* job = m_promise_jobs.take_first(); dbgln_if(PROMISE_DEBUG, "Calling promise job function @ {}", job); @@ -541,10 +541,18 @@ void VM::run_queued_promise_jobs() [[maybe_unused]] auto result = call(*job, js_undefined()); + // This doesn't match the spec, it actually defines that Job Abstract Closures must return + // a normal completion. In reality that's not the case however, and all major engines clear + // exceptions when running Promise jobs. See the commit where these two lines were initially + // added for a much more detailed explanation. + clear_exception(); + stop_unwind(); + if (pushed_execution_context) pop_execution_context(); } // Ensure no job has created a new exception, they must clean up after themselves. + // If they don't, we help a little (see above) so that this assumption remains valid. VERIFY(!m_exception); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.then.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.then.js index 804d133865..7f41b12bbe 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.then.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.prototype.then.js @@ -140,4 +140,31 @@ describe("normal behavior", () => { runQueuedPromiseJobs(); expect(fulfillmentValue).toBe("Some value"); }); + + test("PromiseCapability with throwing resolve function", () => { + class PromiseLike { + constructor(executor) { + executor( + () => { + throw new Error(); + }, + () => {} + ); + } + } + + let resolvePromise; + const p = new Promise(resolve => { + resolvePromise = resolve; + }); + p.constructor = { [Symbol.species]: PromiseLike }; + + // Triggers creation of a PromiseCapability with the throwing resolve function passed to the executor above + p.then(() => {}); + + // Crashes when assuming there are no job exceptions (as per the spec) + // If we survive this, the exception has been silently ignored + resolvePromise(); + runQueuedPromiseJobs(); + }); });