From e0aa2ff5edd1f5ea9a679fab5a705ea69e898275 Mon Sep 17 00:00:00 2001 From: Javier Garcia <93437997+IamYipi@users.noreply.github.com> Date: Wed, 30 Sep 2026 05:20:39 +0200 Subject: [PATCH] fix(engine): return the callback's promise from Promise.try when it can ECMA-262 now calls the callback before making a promise capability, and answers a normal completion with PromiseResolve(ctor, value), so a promise that the same constructor made is returned as is instead of being wrapped in a new one (tc39/ecma262#3883). A throw still rejects a new promise, made after the call. --- core/engine/src/builtins/promise/mod.rs | 48 +++++++--------------- core/engine/src/builtins/promise/tests.rs | 49 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/core/engine/src/builtins/promise/mod.rs b/core/engine/src/builtins/promise/mod.rs index 3893ce8eeaa..df897637951 100644 --- a/core/engine/src/builtins/promise/mod.rs +++ b/core/engine/src/builtins/promise/mod.rs @@ -474,12 +474,12 @@ impl Promise { &self.state } - /// [`Promise.try ( callbackfn, ...args )`][spec] + /// [`Promise.try ( callback, ...args )`][spec] /// - /// Calls the given function and returns a new promise that is resolved if the function - /// completes normally and rejected if it throws. + /// Calls the given function and returns a promise rejected with what it throws, or resolved + /// with what it returns. A returned promise made by the same constructor is returned as is. /// - /// [spec]: https://tc39.es/proposal-promise-try/#sec-promise.try + /// [spec]: https://tc39.es/ecma262/#sec-promise.try pub(crate) fn r#try( this: &JsValue, args: &[JsValue], @@ -488,43 +488,25 @@ impl Promise { let callback = args.get_or_undefined(0); let callback_args = args.get(1..).unwrap_or(&[]); - // 1. Let C be the this value. - // 2. If C is not an Object, throw a TypeError exception. + // 1. Let ctor be the this value. + // 2. If ctor is not an Object, throw a TypeError exception. let c = this.as_object().ok_or_else(|| { JsNativeError::typ().with_message("Promise.try() called on a non-object") })?; - // 3. Let promiseCapability be ? NewPromiseCapability(C). - let promise_capability = PromiseCapability::new(&c, context)?; - - // 4. Let status be Completion(Call(callbackfn, undefined, args)). + // 3. Let status be Completion(Call(callback, undefined, args)). let status = callback.call(&JsValue::undefined(), callback_args, context); match status { - // 5. If status is an abrupt completion, then - Err(err) => { - let value = err.into_opaque(context)?; - - // a. Perform ? Call(promiseCapability.[[Reject]], undefined, « status.[[Value]] »). - promise_capability.functions.reject.call( - &JsValue::undefined(), - &[value], - context, - )?; - } - // 6. Else, - Ok(value) => { - // a. Perform ? Call(promiseCapability.[[Resolve]], undefined, « status.[[Value]] »). - promise_capability.functions.resolve.call( - &JsValue::undefined(), - &[value], - context, - )?; - } + // 4. If status is an abrupt completion, then + // a. Let promiseCapability be ? NewPromiseCapability(ctor). + // b. Perform ? Call(promiseCapability.[[Reject]], undefined, « status.[[Value]] »). + // c. Return promiseCapability.[[Promise]]. + Err(err) => Self::promise_reject(&c, err, context).map(JsValue::from), + // 5. Else, + // a. Return ? PromiseResolve(ctor, ! status). + Ok(value) => Self::promise_resolve(&c, value, context).map(JsValue::from), } - - // 7. Return promiseCapability.[[Promise]]. - Ok(promise_capability.promise.clone().into()) } /// [`Promise.withResolvers ( )`][spec] diff --git a/core/engine/src/builtins/promise/tests.rs b/core/engine/src/builtins/promise/tests.rs index f074e493230..c320bc0c5eb 100644 --- a/core/engine/src/builtins/promise/tests.rs +++ b/core/engine/src/builtins/promise/tests.rs @@ -1,4 +1,5 @@ use crate::{TestAction, run_test_actions}; +use boa_macros::js_str; use indoc::indoc; #[test] @@ -98,3 +99,51 @@ fn promise_race_resolves_first() { TestAction::assert_eq("val", 10), ]); } + +#[test] +fn promise_try_returns_promise_of_same_constructor_as_is() { + run_test_actions([ + TestAction::run(indoc! {r#" + var p = Promise.resolve(1); + class SubPromise extends Promise {} + var sub = SubPromise.resolve(2); + "#}), + TestAction::assert("Promise.try(() => p) === p"), + TestAction::assert("SubPromise.try(() => sub) === sub"), + // A promise from another constructor is still wrapped. + TestAction::assert("Promise.try(() => sub) !== sub"), + TestAction::assert("SubPromise.try(() => p) !== p"), + TestAction::assert("SubPromise.try(() => p) instanceof SubPromise"), + ]); +} + +#[test] +fn promise_try_constructs_only_after_calling_the_callback() { + run_test_actions([ + TestAction::run(indoc! {r#" + var log = []; + function Ctor(executor) { + log.push("construct"); + return new Promise(executor); + } + Promise.try.call(Ctor, () => { log.push("callback"); return 1; }); + Promise.try.call(Ctor, () => { log.push("callback"); throw 2; }); + "#}), + TestAction::assert_eq( + "log.join()", + js_str!("callback,construct,callback,construct"), + ), + ]); +} + +#[test] +fn promise_try_rejects_with_the_thrown_value() { + run_test_actions([ + TestAction::run(indoc! {r#" + var err = null; + Promise.try(() => { throw 3; }).catch(e => { err = e; }); + "#}), + TestAction::inspect_context(|ctx| ctx.run_jobs().unwrap()), + TestAction::assert_eq("err", 3), + ]); +}