diff --git a/lib/InternalBytecode/01-Promise.js b/lib/InternalBytecode/01-Promise.js index ea28bd2da99..6300892b0d3 100644 --- a/lib/InternalBytecode/01-Promise.js +++ b/lib/InternalBytecode/01-Promise.js @@ -397,6 +397,15 @@ }; core.prototype.finally = function (f) { + // Per ECMA-262 §27.2.5.3 (Promise.prototype.finally), when onFinally + // is not callable it must be passed straight through to then(), so + // the resulting Promise simply mirrors the receiver. Without this + // guard, calling f() below throws "undefined is not a function" for + // the no-arg / undefined / null cases that V8, JavaScriptCore and + // SpiderMonkey all accept as passthrough. + if (typeof f !== 'function') { + return this.then(f, f); + } return this.then(function (value) { return core.resolve(f()).then(function () { return value; diff --git a/test/hermes/promise-finally.js b/test/hermes/promise-finally.js new file mode 100644 index 00000000000..44e6eea5dc9 --- /dev/null +++ b/test/hermes/promise-finally.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// RUN: %hermes -Xes6-promise %s | %FileCheck --match-full-lines %s +// RUN: %hermes -Xmicrotask-queue %s | %FileCheck --match-full-lines %s +// RUN: %hermesc -O -emit-binary -out %t.hbc %s && %hermes -Xes6-promise %t.hbc | %FileCheck --match-full-lines %s + +// Regression test for Promise.prototype.finally with a non-callable +// onFinally. Per ECMA-262 §27.2.5.3, when IsCallable(onFinally) is false +// the value/rejection of the receiver must pass straight through. + +print('promise-finally'); +// CHECK-LABEL: promise-finally + +// 1) No-arg call: value passes through. +Promise.resolve('ok').finally().then(function (v) { + print('no-arg:', v); +}); +// CHECK-NEXT: no-arg: ok + +// 2) Explicit undefined: value passes through. +Promise.resolve('ok').finally(undefined).then(function (v) { + print('undefined:', v); +}); +// CHECK-NEXT: undefined: ok + +// 3) null: value passes through. +Promise.resolve('ok').finally(null).then(function (v) { + print('null:', v); +}); +// CHECK-NEXT: null: ok + +// 4) Rejection passes through unchanged when onFinally is not callable. +Promise.reject(new Error('x')).finally().then(function () { + print('rejection: unexpected resolve'); +}, function (e) { + print('rejection:', e.message); +}); +// CHECK-NEXT: rejection: x + +// 5) Sanity: callable onFinally still works (no regression). +Promise.resolve('ok').finally(function () {}).then(function (v) { + print('callable:', v); +}); +// CHECK-NEXT: callable: ok