Skip to content

then, catch, and finally

A Promise by itself does not do anything useful — you need to tell it what to do when it settles. The three methods for this are .then(), .catch(), and .finally().

.then(onFulfilled) registers a callback that runs when the Promise fulfills. The callback receives the resolved value:

const promise = new Promise(resolve => {
setTimeout(() => resolve(42), 500);
});
promise.then(value => {
console.log(value); // 42 — runs 500ms later
});

.then() returns a new Promise, making it chainable. The value returned from the .then callback becomes the resolved value of the next Promise in the chain:

promise
.then(value => value * 2) // 84
.then(value => value + 1) // 85
.then(value => console.log(value)); // 85

Each .then transforms the value and passes it to the next.

.catch(onRejected) registers a callback that runs when the Promise rejects. It is shorthand for .then(null, onRejected):

const badPromise = new Promise((resolve, reject) => {
reject(new Error('Something went wrong'));
});
badPromise
.then(value => console.log('Success:', value))
.catch(error => console.error('Error:', error.message));
// Error: Something went wrong

.catch() placed at the end of a chain catches any rejection that propagated up the chain — from the original Promise or from any .then callback that threw:

fetch('https://invalid-url-that-does-not-exist.example')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Request failed:', error.message));

If the fetch fails, or if response.json() throws, .catch handles it.

.finally(callback) runs after the Promise settles, regardless of whether it fulfilled or rejected. Use it for cleanup that must happen either way:

showLoadingSpinner();
fetch('https://open.er-api.com/v6/latest/USD')
.then(response => response.json())
.then(data => displayRates(data.rates))
.catch(error => showErrorMessage(error.message))
.finally(() => hideLoadingSpinner());

The spinner hides whether the request succeeded or failed. .finally does not receive the value or error — it just runs.

The standard Promise chain for a fetch call has three steps:

fetch(url)
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => {
// use data here
})
.catch(error => {
// handle error here
});

Note the first .then: it checks response.ok before calling .json(). fetch only rejects on network failure — a 404 or 500 response is still a “fulfilled” Promise. You have to check response.ok yourself and throw to propagate it as an error.

What you return from .then matters:

  • Return a plain value → next .then receives it
  • Return a Promise → next .then waits for that Promise to resolve
  • Throw an error → execution jumps to the next .catch
fetch(url)
.then(response => response.json()) // returns a Promise — chain waits for it
.then(data => data.rates) // returns a plain value
.then(rates => console.log(rates)); // receives the rates object
  1. Create a Promise that resolves to 'BudgetBuddy' after 1 second. Use .then to log the value.
  2. Create a Promise that rejects with new Error('No data'). Chain a .catch to log the error message.
  3. Using the real ExchangeRate-API (https://open.er-api.com/v6/latest/USD), write a fetch chain that:
    • Checks response.ok and throws if false
    • Parses the JSON
    • Logs the rates.EUR value
    • Catches any error and logs it
  4. Add .finally to the chain from step 3 that logs 'Request complete' regardless of outcome.
  • .then(fn) runs fn when the Promise fulfills; returns a new Promise with the callback’s return value.
  • .catch(fn) runs fn when the Promise rejects — handles errors anywhere in the chain.
  • .finally(fn) runs fn after the Promise settles — good for cleanup like hiding spinners.
  • fetch only rejects on network failure; check response.ok manually for HTTP errors.
  • Return a Promise from .then to wait for it; throw an error to jump to .catch.