Behavior of Promise.finally

February 28th 2020 JavaScript TypeScript

Recently, we discussed the behavior of Promise.finally in our development team. Even after reading the official documentation, we weren't unanimous on what will happen in certain cases. In the end, I wrote a couple of test cases to make sure about it.

Even before the argument started, we were all (correctly) convinced that the finally callback will be invoked both when the underlying promise is resolved or rejected:

test('finally is invoked for a resolved promise', () => {
  expect.assertions(1);
  let invoked = false;
  const promise = Promise.resolve().finally(() => {
    invoked = true;
  });
  return promise.then(() => {
    expect(invoked).toBe(true);
  });
});

test('finally is invoked for a rejected promise', () => {
  expect.assertions(1);
  let invoked = false;
  const promise = Promise.reject().finally(() => {
    invoked = true;
  });
  return promise.catch(() => {
    expect(invoked).toBe(true);
  });
});

Successful execution of the callback will not affect the outcome of the underlying promise. Most importantly, a rejected promise will remain rejected even if the finally callback succeeds (unlike the catch callback):

test("finally doesn't affect the outcome of a resolved promise", () => {
  expect.assertions(1);
  const promise = Promise.resolve(42).finally(() => 1);
  return expect(promise).resolves.toBe(42);
});

test("finally doesn't affect the outcome of a rejected promise", () => {
  expect.assertions(1);
  const promise = Promise.reject(42).finally(() => 1);
  return expect(promise).rejects.toBe(42);
});

However, if the finally callback fails for whatever reason (by throwing an unhandled error or returning a rejected promise), it will change the outcome of the underlying promise to be rejected:

test('finally rejects the promise if it throws an unhandled error', () => {
  expect.assertions(1);
  const promise = Promise.resolve().finally(() => {
    throw new Error('Promise.finally failed');
  });
  return expect(promise).rejects.toMatchObject({
    message: 'Promise.finally failed'
  });
});

test('finally rejects the promise if it returns a rejected promise', () => {
  expect.assertions(1);
  const promise = Promise.resolve().finally(() => {
    return Promise.reject('Promise.finally failed');
  });
  return expect(promise).rejects.toBe('Promise.finally failed');
});

I have a suspicion that these tests might come in handy again in the future.

Copyright
Creative Commons License