Skip to the code
cf.

Promise.prototype.finally

MediumPolyfills

Write Promise.prototype.myFinally(onFinally), which works like the built-in finally without calling it. It returns a new promise, and once the original promise settles, fulfilled or rejected, it calls onFinally with no arguments.

The new promise settles the same way as the original: with the same value, or rejected with the same reason. What onFinally returns is ignored, with one exception: if it returns a promise, the new promise waits for that promise before settling. If onFinally throws, or returns a promise that rejects, the new promise rejects with that error instead.

If onFinally isn't a function, the result passes through unchanged.

Promise.resolve("data")
  .myFinally(() => "ignored")
  .then((value) => console.log(value)); // "data"
 
Promise.reject(new Error("offline"))
  .myFinally(() => console.log("hide spinner"))
  .catch((error) => console.log(error.message)); // "hide spinner", then "offline"

What the tests check

  1. returns a new promise
    const original = Promise.resolve(1);
    const result = original.myFinally(() => {});
    expect(result).toBeInstanceOf(Promise);
    expect(result).not.toBe(original);
    
  2. calls onFinally with no arguments when the promise fulfills
    const onFinally = spy();
    await Promise.resolve("value").myFinally(onFinally);
    expect(onFinally).toHaveBeenCalledTimes(1);
    expect(onFinally.calls[0].args).toEqual([]);
    
  3. calls onFinally with no arguments when the promise rejects
    const failed = Promise.reject(new Error("failed"));
    failed.catch(() => {}); // so a broken myFinally can't leave it unhandled
    const onFinally = spy();
    await failed.myFinally(onFinally).catch(() => {});
    expect(onFinally).toHaveBeenCalledTimes(1);
    expect(onFinally.calls[0].args).toEqual([]);
    
  4. keeps the original value
    await expect(Promise.resolve("value").myFinally(() => "ignored")).resolves.toBe("value");
    
  5. keeps the original rejection reason
    const error = new Error("original");
    const failed = Promise.reject(error);
    failed.catch(() => {}); // so a broken myFinally can't leave it unhandled
    await expect(failed.myFinally(() => "ignored")).rejects.toBe(error);
    
  6. waits for a promise that onFinally returns
    const clock = useFakeTimers();
    let settled = false;
    const result = Promise.resolve("done").myFinally(() => new Promise((resolve) => setTimeout(resolve, 100)));
    const markSettled = () => {
      settled = true;
    };
    result.then(markSettled, markSettled);
    await clock.tickAsync(99);
    expect(settled).toBe(false);
    await clock.tickAsync(1);
    expect(settled).toBe(true);
    await expect(result).resolves.toBe("done");
    
  7. rejects with the error when onFinally throws or its promise rejects
    const thrown = new Error("cleanup threw");
    await expect(
      Promise.resolve(1).myFinally(() => {
        throw thrown;
      }),
    ).rejects.toBe(thrown);
    const rejected = new Error("cleanup rejected");
    const cleanup = Promise.reject(rejected);
    cleanup.catch(() => {}); // so a myFinally that ignores it can't leave it unhandled
    await expect(Promise.resolve(1).myFinally(() => cleanup)).rejects.toBe(rejected);
    const failed = Promise.reject(new Error("first"));
    failed.catch(() => {}); // so a broken myFinally can't leave it unhandled
    await expect(
      failed.myFinally(() => {
        throw thrown;
      }),
    ).rejects.toBe(thrown);
    
  8. passes the result through when onFinally isn't a function
    await expect(Promise.resolve(1).myFinally()).resolves.toBe(1);
    await expect(Promise.resolve(2).myFinally("not a function")).resolves.toBe(2);
    const error = new Error("kept");
    const failed = Promise.reject(error);
    failed.catch(() => {}); // so a broken myFinally can't leave it unhandled
    await expect(failed.myFinally(null)).rejects.toBe(error);
    
  9. works without the built-in finally
    const builtIn = Promise.prototype.finally;
    Promise.prototype.finally = function () {
      throw new Error("myFinally called the built-in finally");
    };
    let result;
    try {
      result = Promise.resolve("value").myFinally(() => {});
    } finally {
      Promise.prototype.finally = builtIn;
    }
    await expect(result).resolves.toBe("value");
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).