Skip to main content

Command Palette

Search for a command to run...

finally() in JavaScript: Why It Can't (Usually) Change a Promise's Result

Exploring the Mechanics and Impact of `finally()` on Promise States in JavaScript

Updated
5 min read
finally() in JavaScript: Why It Can't (Usually) Change a Promise's Result

A common misconception is that finally() behaves like then().
In reality, it doesn’t receive values, ignores return values, and only affects a promise in very specific cases. Let’s see why.

finally(): Why It Can’t Change a Promise Result

finally() doesn’t receive arguments

Let's start with a quick quiz to test your understanding:

myPromise.then(() => "Hi, Sasha!")
.finally(res => console.log(res + "Whats up?")) 

//what will appear in console?

If you think the answer is “Hi, Sasha!Whats up?” then keep reading 🙂

How finally() works under the hood

The finally method doesn't have its own low-level implementation in the Promise specification.

Under the hood, it simply delegates to then, using the same callback for both success and failure:

promise.finally(onDone);
// is roughly equivalent to
promise.then(onDone, onDone);

Because of this onDone doesn’t receive any arguments (no resolved value, no rejection reason)

And what happens to the parameter value which wasn’t passed? Correct: it stays undefined.

Correct answer to the quiz is “undefinedWhats up?“ as res value is undefined.

What happens to the Promise value after finally()?

Another quiz:

myPromise.then(() => "Hi, Sasha!")
.finally(res => "my new result")
.then(res => console.log(res)) //what will be logged here?

The answer is "Hi, Sasha!"

The value returned from finally() is simply ignored.

This is a key rule: finally() doesn’t change Promise’s state.

It exists for side effects only - cleanup logic, metrics, etc.

Why the returned value from finally() is ignored

  • finally() doesn’t receive the resolved value

  • it can’t pass a new value down the chain

  • the Promise continues with the value from the last then() or catch()

So this:

.finally(() => "new value")

is effectively the same as:

.finally(() => {})

Takeaways so far

  • Since finally() doesn't get any arguments, any value given to it stays undefined.

  • The return value from a finally() callback is ignored and doesn't change the Promise's state or the next then() chain

  • The Promise keeps the value from the last then() or catch() before finally() was called.

Can finally() change the Promise state?

Promise.resolve("OK")
  .finally(() => {
    throw new Error("Something went wrong");
  })
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

//what is the Promise state at this point?
//what will be logged? "OK" or "Something went wrong"?

At first glance, you might expect "OK".

After all, we already know that finally() doesn’t change a Promise’s value, right?

But in this case, the output will be “Something went wrong” and the Promise ends up rejected.

Why does this happen?

The important clarification is this:

finally() does not change a Promise’s state by default.

However, if an error is thrown inside finally(), or if it returns a rejected Promise, that error overrides the previous state.

Here is another quick quiz:

Promise.resolve("OK")
  .finally(() => {
    return Promise.reject("Something went wrong")
  })
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

//what is the Promise state at this point?
//what will be logged? "OK" or "Something went wrong"?

We are not throwing any error here. However, we are still getting the "Something went wrong" here.

Another way to change a Promise’s state is to return a rejected Promise from finally().

The takeaway we had previously is that the return value from a finally() callback is ignored, so why returning rejected Promise work?

Let’s investigate what happens under the hood.

Remember how finally() works?

promise.finally(onDone);
// is roughly equivalent to
promise.then(onDone, onDone);

Knowing that, let’s investigate our case:

Promise.resolve("OK")
  .finally(() => {
    return Promise.reject("Something went wrong")
  })
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

//is roughly the same as:


Promise.resolve("OK")
//this is our finally method
.then(

// first onDone callback
  value => Promise.resolve(
    (() => { return Promise.reject("Something went wrong"); })()
  ).then(() => value), // if resolves, continue with the previous value

// second onDone callback
  reason => Promise.resolve(
    (() => { return Promise.reject("Something went wrong"); })()
//throwing error
  ).then(() => { throw reason; }) // if gets rejected, continue with rejection reason as value
)
.then(res => console.log(res)) //this is ignored 
.catch(err => console.log(err)); //this is called

Remember that throwing error changes Promise’s state?

This is what is done behind the scenes when we return a rejected Promise.

Note that if you return a new resolved promise from finally(), nothing changes. The chain waits for it to resolve, and then() continues with the original value.

Final takeaways

  • Since finally() doesn't receive any arguments, any value provided to it remains undefined.

  • The return value from a finally() callback is ignored and does not alter the Promise's state or affect the subsequent then() chain.

  • The Promise retains the value from the last then() or catch() before finally() was invoked, unless:

    • The method returns a rejected Promise.

    • An error is thrown within the method.