By the end of this lesson, you will be able to create, chain, and handle JavaScript Promises so asynchronous code reads as a sequence of eventual results instead of nested callbacks.
What it is
A Promise is an object that represents a value that may not be available yet. It is a cleaner model for eventual results: instead of passing a callback into every asynchronous function, the function returns a promise that can later become fulfilled with a value or rejected with an error.
A promise has three states: pending, fulfilled, and rejected. Once it is fulfilled or rejected, it is settled and cannot change state. Related terms include then(), catch(), finally(), async/await, and the promise executor function passed to new Promise().
Why it matters
- It flattens callback nesting, making asynchronous flows easier to read.
- It gives a standard return type for asynchronous APIs, such as
fetch(). - It separates success handling from error handling through chaining.
- It enables composition with helpers like
Promise.all()andPromise.race(). - It is the foundation for
async/await, which lets you write promise-based code in a synchronous style.
Syntax or steps
The smallest useful pattern is to return a promise and attach handlers:
const promise = new Promise((resolve, reject) => {
// asynchronous work
resolve(value);
// or reject(error);
});
promise
.then((value) => {
// success
})
.catch((error) => {
// failure
});
Each then() returns a new promise, so you can chain transformations. If a handler throws an error or returns a rejected promise, the chain jumps to the nearest catch().
For network code, improve the reference example by checking the response and handling errors:
fetch(url)
.then((response) => {
if (!response.ok) throw new Error('HTTP error');
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error(error));
Example
const getUser = (id) => new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: 'Ada' });
} else {
reject(new Error('Invalid user id'));
}
}, 500);
});
getUser(1)
.then((user) => {
console.log('Loaded:', user);
return user.name;
})
.then((name) => {
console.log('Hello,', name);
})
.catch((error) => {
console.error('Failed:', error.message);
})
.finally(() => {
console.log('Request finished');
});
Part by part:
getUser()returns a promise. The executor runs immediately, whilesetTimeout()simulates asynchronous work.resolve()fulfills the promise with the user object;reject()rejects it with an error.- The first
then()receives the fulfilled value and returnsuser.name. - The second
then()receives that returned value because promises chain. catch()handles rejection or any error thrown inside athen().finally()runs after the chain settles, regardless of success or failure.
Common mistakes
- Forgetting to return a value from
then(). If you do not return the next value, the followingthen()receivesundefined. - Not handling rejection. A rejected promise without
catch()can cause an unhandled rejection. Addcatch()at the end of the chain. - Assuming
then()runs immediately. Promise handlers run asynchronously after the current call stack is empty, even if the promise is already settled. - Using
asyncfunctions without awaiting their promises. Calling an async function returns a promise; useawaitorthen()to get the result.
When to use it
Promises are best when you need a composable asynchronous result. Compare them with the closest alternatives:
| Approach | Use when | Trade-off |
|---|---|---|
| Callbacks | Working with older APIs that require callback functions. | Can lead to nesting and inconsistent error handling. |
| Promises | You need chaining, composition, or a standard async return value. | Still uses then() callbacks, though flatter than nested callbacks. |
| async/await | You want promise-based code to read like synchronous code. | Requires an async function and can be less convenient for simple one-step chains. |
Practice
Guided exercise: Write a function delay(ms) that returns a promise resolving after ms milliseconds. Use it with then() to log a message after one second.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
delay(1000).then(() => console.log('One second passed'));
Challenge: Modify delay() so it rejects if ms is negative, then handle the error with catch().
Expected output for delay(-1): an error message such as Invalid delay.
Quick check
Question: What happens if a then() handler throws an error?
Answer: The promise returned by that then() becomes rejected, and the chain continues to the nearest catch() handler.
Summary
A promise is a stand-in for a future value, with clear states and composable handlers. Use promises when asynchronous work needs to be chained, composed, or standardized. Once comfortable, async/await can make the same model easier to read.