Modern web development relies heavily on asynchronous activities to enable responsive, interactive applications. Whether it’s retrieving data from an API, reading files, or running timers, these processes must run in the background without freezing the interface. JavaScript offers you a reliable way to handle these jobs. This article covers all you need to know about promises, including basic ideas and advanced features, to develop error-free asynchronous programs.
In this article you will learn about —
What is a Promise?
Why Use Promises?
How Promises Work?
Handling Promises
Chaining Promises
Error Handling in Promises
Advanced Promise Features
JavaScript Execution Flow with Promises (Important)
Converting Promise Chains to Async/Await
Best Practices and Common Mistakes
What is a Promise?
A Promise in JavaScript is equivalent to making a “promise” to do something in the future. When you make a promise, you are saying, “I promise to give you the results later.” This outcome could be success or failure.
In other words, a promise is an object that reflects the ultimate success (or failure) of an asynchronous operation and its resultant value. It lets you to correlate handlers with the success or failure of an asynchronous action, making your code easier to read and maintainable.
Why Use Promises?
In JavaScript, for instance, time-consuming operations-like retrieving data from a server-were generally accomplished with callbacks. A callback is just a function passed to another function to execute after the task is completed. You might use a callback, for example, to process data when it arrives from a server.
However, when there are complex operations, the use of callbacks get pretty messy. This mess is known as “callback hell,” where one can have a callback within another, and this makes the code unreadable and unmanageable.
Callback Hell Example:
fetchData((data) => {
processData(data, (processedData) => {
saveData(processedData, (result) => {
console.log(result);
});
});
});
As shown above, such code becomes increasingly difficult to read and maintain in larger codebases due to its deeply nested structure, often referred to as “callback hell.”
Promises were introduced to address this problem, offering a cleaner and more organized way to handle asynchronous tasks by allowing chaining in a more readable manner.
Promise-Based Approach:
fetchData()
.then(processData)
.then(saveData)
.then(console.log)
.catch(console.error);
This approach flattens the structure and makes the code more readable and maintainable.
How Promises Work?
Promises in JavaScript can be in one of three states:
Pending: This is the initial step. The promise is still yet to be fulfilled.
Fulfilled: The promise has completed successfully which means it is resolved and has a value.
Rejected: The promise did not complete successfully, and it carries an error message.
Basic Syntax
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise resolved!");
}, 1000);
});
myPromise.then(result => console.log(result));
In this example, the promise resolves after 1 second with the message “Promise resolved!”. The.then() method is used to handle the resolved value.
Handling Promises
Using .then() for Success Handling
The.then()method is used to handle what happens when a promise is successfully completed. It registers functions (callbacks) to run when the promise is fulfilled.
myPromise.then(data => {
console.log("Data received:", data);
});
Using .catch() for Error Handling
The.catch()method is used to handle what happens when a promise fails. It registers a function (callback) to run when the promise is rejected.
myPromise.catch(error => {
console.error("Error:", error);
});
Using .finally() for Cleanup
The.finally()method lets you run some code after the promise is done, whether it was successful or not.
myPromise.finally(() => {
console.log("Cleanup tasks");
});
Chaining Promises
Chaining allows you to perform tasks sequentially by passing the outcome of the previous one. Then proceed to the next.then(). This allows you to handle several asynchronous tasks sequentially.
Example of Chaining:
fetch('https://api.example.com/user')
.then(response => response.json())
.then(data => {
console.log("Processed data:", data);
return processData(data);
})
.then(finalResult => {
console.log("Final result:", finalResult);
})
.catch(error => console.error("Error:", error));
This example uses each.then()to handle each step in the process, allowing for clear data flow. This allows you to see how the result of one stage is transferred to the next.
Error Handling in Promises
Promises simplify error handling by allowing them to pass down the chain to the.catch()method for resolution. This eliminates the need to handle failures at each phase, keeping your code clearer and easier to manage.
Example with Error Propagation:
fetchData()
.then(processData)
.then(saveData)
.catch(error => console.error("An error occurred:", error));
If any step in the promise chain fails, the error will be caught by the.catch()block. This makes it easy to handle issues and keep your code running smoothly.
Advanced Promise Features
1. Promise.all() for Parallel Execution
The Promise.all()method allows you to run several promises simultaneously and wait for them all to complete. If all of the promises are fulfilled, you will receive the results of each one. If any promise fails, it detects the mistake.
Promise.all([fetchData1(), fetchData2(), fetchData3()])
.then(results => {
console.log("All data fetched:", results);
})
.catch(error => console.error("Error fetching data:", error));
In this example, if any promise fails, the entire Promise.all() fails.
2. Promise.race() for Fastest Promise
The Promise.race() method returns the result of the first promise that finishes, whether it succeeds or fails.
Promise.race([fetchData1(), fetchData2()])
.then(result => console.log("First to finish:", result));
In this example, whichever promise (fetchData1 or fetchData2) completes first will have its result logged to the console.
3. Promise.allSettled() for Handling All Outcomes
The Promise.allSettled()method waits for all the promises you give it to be in a successful or failed state and then finish. An array is then returned that has the results of each promise.
Promise.allSettled([fetchData1(), fetchData2()])
.then(results => {
results.forEach(result => console.log(result.status, result.value || result.reason));
});
In this example, Promise.allSettled() waits for both fetchData1() and fetchData2() to complete. It then logs the status and result (or error) of each promise. This way, you can see what happened with each promise, regardless of whether they succeeded or failed.
4.Promise.any() for Resolving with the First Successful Promise
The Promise.any() method waits for the first promise to be resolved correctly from a list of promises. In case at least one promise is resolved, the value will be returned by the Promise.any() method. If all promises are refused, this method will throw an error.
const promise1 = new Promise((resolve, reject) => setTimeout(reject, 100, 'Error 1'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 200, 'Success A'));
const promise3 = new Promise((resolve, reject) => setTimeout(reject, 300, 'Error 3'));
Promise.any([promise1, promise2, promise3])
.then((value) => console.log(value)) // logs: 'Success A'
.catch((error) => console.error('All promises were rejected', error));
In this example, Promise.any() waits for the first promise to be resolved successfully. The procedure returns the outcome of the first successful promise, in this case promise2with the value ‘Success A’. If all promises are refused, the.catch() block is executed, logging the error message. This strategy is beneficial when you want to receive the result of the first successful promise without having to wait for the rest.
JavaScript Execution Flow with Promises (Important)
1. Promises in JavaScript run in the microtask queue, which gets priority over macrotasks like setTimeout.
Here’s an example to illustrate this:
console.log(2);
setTimeout(() => console.log(4), 0);
Promise.resolve().then(() => console.log(3));
console.log(6);
// 2
// 6
// 3
// 4
In this example:
console.log(2) runs first because it's a regular synchronous operation.
console.log (6) runs next because it's also synchronous.
The promise’s.then()runs before the setTimeout callback because promises are microtasks, which have higher priority, hence prints 3.
Finally, the setTimeout callback runs, as it's a macrotask and prints 4.
So always remember, the promise’s.then()executes before the setTimeout callback due to the microtask queue's priority.
2. Promise Execution Order and the Microtask Queue with Multiple .then() Calls
In JavaScript, code runs in a specific order: first the synchronous code, then microtasks (like promises), and finally, macrotasks (likesetTimeout).
Here’s one example to explain this:
console.log(3)
const promise = new Promise((resolve) => {
console.log(6)
resolve()
console.log(2)
})
console.log(7)
promise.then(() => {
console.log(1)
}).then(() => {
console.log(9)
})
console.log(8)
setTimeout(() => {
console.log(13)
}, 10)
setTimeout(() => {
console.log(21)
}, 0)
// 3
// 6
// 2
// 7
// 8
// 1
// 9
// 21
// 13
In this example, synchronous code runs first, logging 3, 6, 2, 7, and 8. Once the synchronous code finishes, microtasks (the.then() callbacks) are processed, logging 1 and 9. Finally, macrotasks (from setTimeout) execute in order of their delays, logging 21 (0ms) and 13 (10ms). This highlights JavaScript's execution order: synchronous code > microtasks > macrotasks.
3. Multiple Resolve and Reject Calls in a Promise: Only the First One Matters
When you create a promise, the first call to resolve or reject is the only one that counts. All the other calls are dismissed.
Here’s an example to illustrate this:
new Promise((resolve, reject) => {
resolve(1); // This will be the only one that counts
resolve(2); // Ignored
reject('error'); // Ignored
}).then((value) => {
console.log(value); // Logs: 1
}, (error) => {
console.log('error'); // This won't run
});
In this example, the promise is resolved with the value 1. The second resolve and the reject calls are ignored because the promise has already been settled with the first resolve.
4. Chaining Promises and Handling Values in Sequential .then() Calls
When you chain promises, each.then() handles a step in the process.
Promise.resolve(1) // 1
.then(() => 2) // 2 (here 1 goes to the .then method but isn't used)
.then(3) // skip
.then((value) => value * 3) // 2 * 3 = 6
.then(Promise.resolve(4)) // creates a Pending promise
.then(console.log) // console.log will display 6
In this example, Promise.resolve(1) starts with a value of 1, but the first .then(() => 2) returns 2 instead. The next .then(3) is ignored, and the value 2 is passed on. The .then((value) => value * 3) multiplies the value by 3, resulting in 6. The .then(Promise.resolve(4)) doesn’t change the value, and finally, .then(console.log) logs 6. This demonstrates how values are passed through the chain, with non-function values being ignored.
5. Promise Chain with .catch() and .finally() Handling
Promise.resolve(1)
.then((val) => {
console.log(val) // resolve with value 1
return val + 1 // return 2
}).then((val) => {
console.log(val) // 2
// return undefined
}).then((val) => {
console.log(val) // undefined
return Promise.resolve(3)
.then((val) => {
console.log(val) // 3
// return undefined
})
}).then((val) => {
console.log(val) // undefined
return Promise.reject(4) // return 4
}).catch((val) => {
console.log(val) // 4
// return undefined
}).finally((val) => {
console.log(val) // undefined: finally has no arguments
return 10 // no effect on promise object
}).then((val) => {
console.log(val) // undefined: because recent 'catch()' handled the promise object with 'undefined'
})
In this example, we’re chaining multiple.then(),.catch(), and.finally() methods together to show how different stages of promise resolution are handled. Let's break it down:
finally() does not receive an argument:
The finally() block executes clean-up code but doesn't take or pass any values. It’s used to ensure certain code runs regardless of the promise’s outcome.Returning a value in finally() doesn't affect the promise:
If you return a value in the finally() block, it doesn't affect the promise chain or the final value. It's executed after the promise resolution/rejection but doesn't modify the result.Throwing an error in finally() causes rejection:
If you throw an error or return a rejected promise in finally(), it will cause the promise chain to reject with the error or rejection reason.
Promise.reject(1)
.finally(() => { throw new Error(2); });
OR
Promise.reject(1)
.finally(() => { return Promise.reject(2); });
- The order of then() and catch() matters The .then() and .catch() can be invoked in any order, but they will always return the promise’s final state. When a promise is handled by .catch(), any subsequent .then() will receive the final value.
Example:
Promise.reject(1)
.catch((val) => { console.log(val); }) // 1
.then((val) => { console.log(val); }); // undefined
Converting Promise Chains to Async/Await
Async/await is a method for using promises that causes the code to become more like the code written in synchronous mode. The term often used is “syntactic sugar” because it gives a more straightforward and cleaner path of doing asynchronous code.
async function fetchData() {
try {
const response = await fetch('https://api.example.com/user');
const data = await response.json();
console.log("Data:", data);
} catch (error) {
console.error("Error:", error);
}
}
Combining Promises with Async/Await
You can combine promises with async/await for parallel execution using Promise.all().
async function getAllData() {
try {
const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
console.log("Data 1:", data1);
console.log("Data 2:", data2);
} catch (error) {
console.error("Error fetching data:", error);
}
}
Best Practices and Common Mistakes
Avoid Deep Nesting: Use chaining or async/await to keep code flat and readable.
Always Handle Errors: Make sure every promise chain has a.catch() or a try/catch block.
Use Parallel Execution Wisely: Only use Promise.all() when tasks are independent but need to finish together.
Conclusion
JavaScript promises are one of the best ways to deal with your time-consuming operations, such as, retrieving data on a server. They even help you write cleaner easier-to-maintain code, and the practice of what you have learned will equip you to take full advantage of asynchronous coding. Once you get some hands-on experience and begin handling errors elegantly, promises will become such a huge part of JavaScript.
Thank you for reading! If you find this article helpful, feel free to highlight, clap, leave a comment, or even reach out to me on Twitter/X and LinkedIn as it’s very appreciated and helps keeps content like this free!
Top comments (0)