Picture yourself planning a large event with multiple dependencies. You need a caterer to confirm the menu, a florist to handle the decorations, and a DJ for music. Each vendor operates on their own timeline, so you don’t want to wait on one before contacting the next—you want them to proceed in parallel or in a structured order without creating a tangled mess of back-and-forth calls. JavaScript promises offer a similar convenience for handling asynchronous operations, preventing you from getting stuck in callback hell.
In this lesson, you'll discover:
then and catchLet’s say you want to:
If you rely purely on callbacks, you may wind up nesting them like Russian dolls:
function getForecastForLocation() {
locationRequest({
success: spotRequest({
success: forecastRequest({
success: handleSuccess,
error: handleError
}),
error: handleError
}),
error: handleError
});
}
This can lead to deeply nested code that’s tough to read, debug, and maintain. That dreaded “callback hell” is like having multiple event planners constantly trying to schedule each other in the correct order, each with their own error handling.
Promises provide a cleaner, more linear way to handle asynchronous flows. They let you chain successive actions instead of nesting callbacks deeply. Here’s how you might transform the above code using promises:
function getForecastForLocation() {
locationRequest()
.then(spotRequest)
.then(forecastRequest)
.then(handleSuccess)
.catch(handleError);
}
With promises:
then seamlessly passes the result of one function into the next.catch centralizes error handling, removing the need for sprinkling
error callbacks everywhere.
Think of it like a well-organized line of tasks: each step hands off its completed work to the next step, and if something fails, everything diverts to a single error management plan.
It helps to understand a few terms:
A promise can’t be fulfilled and then become rejected later, or vice versa. Once it’s settled, that’s the final outcome. If callbacks are added after it has already settled, those callbacks will immediately execute (assuming they match the state).
You can create a new promise using the Promise constructor function.
It accepts an executor function that has two parameters: resolve
and reject.
const p = new Promise((resolve, reject) => {
// If everything goes well
if (/* success condition */) {
resolve(/* some data */);
} else {
reject(/* some error */);
}
});
resolve is like telling the promise, “All good—here’s the result.”
reject is like saying, “We had a problem—here’s the error.”
const request = new Promise(resolve => {
setTimeout(() => resolve('Response received!'), 1000);
});
request.then(msg => console.log(msg));
// After one second -> logs: "Response received!"
In this snippet, then is invoked once setTimeout completes
after one second, passing "Response received!" into the callback.
then and catchA promise object has two critical methods:
then(onFulfilled, onRejected)catch(onRejected)
then returns another promise, which is why you can chain them together.
It’s like each successful step queues up for the next.
p.then(onFulfilled)
.then(onFulfilledNext)
.catch(onRejected);
catch is essentially shorthand for then(null, onRejected), letting
you handle errors in one place rather than sprinkling them everywhere.
Example:
p.then(result => {
console.log('Got result:', result);
return doAnotherAsyncTask();
}).catch(error => {
console.error('Error happened:', error);
});
If p fails, the catch callback triggers; if doAnotherAsyncTask()
fails, the error also flows to the same catch.
Many libraries and frameworks have features that return promises. For example, jQuery’s
$.ajax method can be used like a promise (it actually returns a
jqXHR object, but it behaves similarly).
const fetchCat = catId => (
$.ajax({ url: `/cats/${catId}` })
);
fetchCat(1)
.then(cat => console.log('Fetched cat:', cat))
.fail(err => console.error('Failed to fetch cat:', err));
Notice that fail is used instead of catch in jQuery. Different
promise-like implementations may have slightly differing method names, but the concept
remains the same—one callback for success, one for error.
Instead of forcing every function to deal with possible failures, you can rely on chained
.then handlers, culminating in a single .catch for errors.
This keeps your code more modular and makes debugging easier.
A function that fetches data doesn't have to know what to do if the data is missing or the network fails— it merely passes any issues on to the promise chain’s error handler.
Promises are a key piece of modern JavaScript, making asynchronous code
more manageable and less error-prone. They allow you to handle success and failure cleanly,
avoiding “callback pyramids of doom.” By chaining then and catch,
you can neatly separate your concerns, orchestrate multiple async tasks, and handle
errors centrally.
Remember:
.then and .catch help you chain success and handle errorsPromise.all is useful for parallel tasksAs you build complex applications with real-time data fetching, user interactions, and asynchronous workflows, mastering promises will save you countless headaches and produce cleaner, more robust code.