jsguides

Promise.prototype.finally()

finally(onFinally)

The finally() method attaches a callback that will be called when the Promise settles — whether it fulfills or rejects. This is useful for cleanup tasks that should run regardless of the outcome, such as hiding loading spinners, closing connections, or releasing resources.

Syntax

promise.finally(onFinally)
promise.finally(() => {
  // cleanup code
})

Parameters

ParameterTypeDefaultDescription
onFinallyfunctionundefinedCalled when the Promise settles (fulfills or rejects). Does not receive any arguments.

Return Value

A new Promise that resolves or rejects with the same result as the original Promise, unless the onFinally callback throws or returns a rejected Promise.

Examples

Basic cleanup

let isLoading = true;

fetch("/api/data")
  .then(data => {
    console.log("Data loaded:", data);
    return data;
  })
  .catch(error => {
    console.error("Error:", error.message);
  })
  .finally(() => {
    isLoading = false;
    console.log("Request complete");
  });

This first example shows the core pattern: set a flag to track loading state, then use .finally() to reset it. The flag is cleared regardless of whether the fetch succeeded or threw an error. Without finally(), you would need to duplicate the isLoading = false assignment inside both .then() and .catch().

Hiding a loading spinner

async function loadUserData() {
  showSpinner();
  
  try {
    const response = await fetch("/api/user");
    const user = await response.json();
    displayUser(user);
  } catch (error) {
    showError(error.message);
  } finally {
    hideSpinner(); // Always runs, regardless of success/failure
  }
}

When using async/await, the native try/catch/finally construct mirrors Promise.prototype.finally() exactly. The finally block runs after both the try (success) and catch (failure) paths complete, making it the natural place for UI cleanup. This async/await pattern is the most common way you will see finally() in modern JavaScript codebases.

Resetting state

let requestInFlight = false;

function makeRequest(url) {
  requestInFlight = true;
  
  return fetch(url)
    .then(response => response.json())
    .finally(() => {
      requestInFlight = false; // Always reset state
    });
}

This wraps a fetch in a function that tracks whether a request is currently in progress. The requestInFlight flag guards against duplicate submissions — common in form submit handlers where a double-click could trigger two requests at once. The flag is set before the request begins and reset in finally(), so it always reflects the current state regardless of network success or failure.

Cleaning up resources

function readFile(filename) {
  const stream = createReadStream(filename);
  
  return new Promise((resolve, reject) => {
    stream.on("data", data => process(data));
    stream.on("end", resolve);
    stream.on("error", reject);
  }).finally(() => {
    stream.destroy(); // Always close the stream
  });
}

Node.js streams and file handles need deterministic cleanup. The .finally() call guarantees stream.destroy() runs even after a read error, avoiding file-descriptor leaks that can crash a long-running server process. This pattern applies to any resource that needs explicit closing: database connections, WebSocket clients, or temporary file handles.

Using with then and catch

fetch("/api/resource")
  .then(response => {
    if (!response.ok) throw new Error("Not found");
    return response.json();
  })
  .then(data => {
    console.log("Got data:", data);
  })
  .catch(error => {
    console.error("Failed:", error.message);
  })
  .finally(() => {
    console.log("Cleaning up...");
    resetUI();
  });

This chains all four methods together: then(), a second then(), catch(), and finally(). Even when the catch() handles the error, finally() still runs — it fires on settle, not on success. The resetUI() call runs whether the resource was found or whether the fetch threw.

Common Mistakes

Expecting arguments

// WRONG: onFinally does not receive arguments
promise.finally(value => {
  console.log(value); // undefined!
});

// CORRECT: Use then or catch for values
promise
  .then(value => {
    console.log("Success:", value);
    return value;
  })
  .catch(error => {
    console.error("Error:", error.message);
    throw error;
  })
  .finally(() => {
    console.log("Done"); // No arguments
  });

A common mistake is expecting finally() to receive the resolved value or rejection reason. It doesn’t. If you need the outcome, use then() or catch() to capture it before finally() runs. Think of finally() as a side-effect hook, not a data-processing step.

Not returning the original promise value

// WRONG: Missing return breaks chaining
fetch("/api/data")
  .then(data => process(data))
  .finally(() => {
    console.log("Done");
  }); // The resolved value is lost!

// CORRECT: finally passes through the value
fetch("/api/data")
  .then(data => process(data))
  .finally(() => {
    console.log("Done");
  })
  .then(processed => {
    console.log(processed); // Still available!
  });

The broken version calls .finally() and stops — nothing captures the resolved value. The corrected version chains another .then() after .finally(), which is legal because .finally() returns a promise that passes the settled value through transparently. Remember this rule: chain through .finally(), never end on it unless you truly don’t need the result.

Using finally for error handling

// WRONG: finally cannot handle errors
promise
  .then(data => {
    throw new Error("Oops");
  })
  .finally(() => {
    console.log("Cleanup"); // Runs
  })
  .catch(e => console.log("Caught:", e.message)); // Still catches!

// finally is for cleanup, not error handling

The catch() after finally() still fires even though finally() ran. That’s by design: finally() is transparent and does not consume the rejection. The error continues propagating, and the downstream catch() is what actually handles it. Putting error-handling logic inside finally() is a design mistake — use catch() for that.

Async functions in finally

// finally can be async but be careful with timing
async function loadData() {
  try {
    const data = await fetchData();
    return data;
  } finally {
    await cleanup(); // Waits before continuing
    console.log("Fully cleaned up");
  }
}

How finally() passes through values

finally() is transparent: it does not receive the resolved value or rejection reason, and unless the onFinally callback throws or returns a rejected promise, the settled state of the original promise passes through unchanged. This means you can insert a finally() anywhere in a chain without disrupting the values flowing through it — the resolved value (or rejection) before the finally() is the same as the one after.

Promise.resolve(42)
  .finally(() => console.log('cleanup'))
  .then(v => console.log(v)); // 42 — value passed through

When finally() is the right tool

finally() is best for actions that should happen after both success and failure. Resetting loading state, closing sockets, and clearing timers are all good examples because the cleanup is not part of the business result itself. That separation keeps your success path and error path focused on their own jobs while still guaranteeing that shared cleanup code runs once.

What finally() cannot do

finally() does not replace catch() and it does not see the settled value. If you need to transform a result, use then(). If you need to handle an error, use catch(). finally() is specifically for after-the-fact cleanup. Thinking of it that way helps prevent a common mistake: putting logic in finally() that actually belongs in a success or error handler.

finally() vs then(fn, fn)

then(fn, fn) with the same function in both handlers is a common workaround for “run on settle,” but it duplicates the function reference. finally(fn) is cleaner, explicitly communicates the intent (cleanup), and guarantees the callback is called exactly once regardless of outcome.

finally() and async/await

In async functions, the equivalent of .finally() is a try/catch/finally block. The finally clause runs after both success and error paths, receives no value, and cannot change the resolved or rejected outcome (unless it throws or returns a promise, which overrides the original). The Promise.prototype.finally() method follows the same semantics for promise chains.

See Also