diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 3a6deb06c84..99950e5bde8 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -614,6 +614,7 @@ struct CommonPropertyNames { PropertyKey register_ { "register", PropertyKey::StringMayBeNumber::No }; PropertyKey return_ { "return", PropertyKey::StringMayBeNumber::No }; PropertyKey throw_ { "throw", PropertyKey::StringMayBeNumber::No }; + PropertyKey try_ { "try", PropertyKey::StringMayBeNumber::No }; PropertyKey union_ { "union", PropertyKey::StringMayBeNumber::No }; PropertyKey xor_ { "xor", PropertyKey::StringMayBeNumber::No }; PropertyKey inputAlias { "$_", PropertyKey::StringMayBeNumber::No }; diff --git a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp index 696e6e8f9a8..3dc85e047a5 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2021-2024, Linus Groh * * SPDX-License-Identifier: BSD-2-Clause */ @@ -238,6 +238,7 @@ void PromiseConstructor::initialize(Realm& realm) define_native_function(realm, vm.names.race, race, 1, attr); define_native_function(realm, vm.names.reject, reject, 1, attr); define_native_function(realm, vm.names.resolve, resolve, 1, attr); + define_native_function(realm, vm.names.try_, try_, 1, attr); define_native_function(realm, vm.names.withResolvers, with_resolvers, 0, attr); define_native_accessor(realm, vm.well_known_symbol_species(), symbol_species_getter, {}, Attribute::Configurable); @@ -458,6 +459,43 @@ JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::resolve) return TRY(promise_resolve(vm, constructor.as_object(), value)); } +// 1 Promise.try ( callbackfn, ...args ), https://tc39.es/proposal-promise-try/#sec-promise.try +JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::try_) +{ + auto callbackfn = vm.argument(0); + Span args; + if (vm.argument_count() > 1) { + args = vm.running_execution_context().arguments.span().slice(1, vm.argument_count() - 1); + } + + // 1. Let C be the this value. + auto constructor = vm.this_value(); + + // 2. If C is not an Object, throw a TypeError exception. + if (!constructor.is_object()) + return vm.throw_completion(ErrorType::NotAnObject, constructor.to_string_without_side_effects()); + + // 3. Let promiseCapability be ? NewPromiseCapability(C). + auto promise_capability = TRY(new_promise_capability(vm, constructor)); + + // 4. Let status be Completion(Call(callbackfn, undefined, args)). + auto status = JS::call(vm, callbackfn, js_undefined(), args); + + // 5. If status is an abrupt completion, then + if (status.is_throw_completion()) { + // a. Perform ? Call(promiseCapability.[[Reject]], undefined, « status.[[Value]] »). + TRY(JS::call(vm, *promise_capability->reject(), js_undefined(), *status.throw_completion().value())); + } + // 6. Else, + else { + // a. Perform ? Call(promiseCapability.[[Resolve]], undefined, « status.[[Value]] »). + TRY(JS::call(vm, *promise_capability->resolve(), js_undefined(), status.value())); + } + + // 7. Return promiseCapability.[[Promise]]. + return promise_capability->promise(); +} + // 27.2.4.8 Promise.withResolvers ( ), https://tc39.es/ecma262/#sec-promise.withResolvers JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::with_resolvers) { diff --git a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.h b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.h index ca151325afd..8a3142a244f 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.h +++ b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.h @@ -33,6 +33,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(reject); JS_DECLARE_NATIVE_FUNCTION(resolve); JS_DECLARE_NATIVE_FUNCTION(symbol_species_getter); + JS_DECLARE_NATIVE_FUNCTION(try_); JS_DECLARE_NATIVE_FUNCTION(with_resolvers); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.try.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.try.js new file mode 100644 index 00000000000..9be14b49adf --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.try.js @@ -0,0 +1,63 @@ +describe("errors", () => { + test("this value must be a constructor", () => { + expect(() => { + Promise.try.call({}); + }).toThrowWithMessage(TypeError, "[object Object] is not a constructor"); + }); +}); + +describe("normal behavior", () => { + test("length is 1", () => { + expect(Promise.try).toHaveLength(1); + }); + + test("returned promise is a Promise", () => { + const fn = () => {}; + const promise = Promise.try(fn); + expect(promise).toBeInstanceOf(Promise); + }); + + test("returned promise is resolved when function completes normally", () => { + const fn = () => {}; + const promise = Promise.try(fn); + + let fulfillmentValue = null; + promise.then(value => { + fulfillmentValue = value; + }); + + runQueuedPromiseJobs(); + + expect(fulfillmentValue).toBe(undefined); + }); + + test("returned promise is rejected when function throws", () => { + const fn = () => { + throw "error"; + }; + const promise = Promise.try(fn); + + let rejectionReason = null; + promise.catch(value => { + rejectionReason = value; + }); + + runQueuedPromiseJobs(); + + expect(rejectionReason).toBe("error"); + }); + + test("arguments are forwarded to the function", () => { + const fn = (...args) => args; + const promise = Promise.try(fn, "foo", 123, true); + + let fulfillmentValue = null; + promise.then(value => { + fulfillmentValue = value; + }); + + runQueuedPromiseJobs(); + + expect(fulfillmentValue).toEqual(["foo", 123, true]); + }); +});