8. Promises and the Microtask Queue
Promises are a fundamental part of modern JavaScript, providing a powerful way to handle asynchronous operations. They represent a value that may be available now, later, or never, allowing developers to write cleaner and more manageable asynchronous code. Promises interact closely with the microtask queue, ensuring that promise resolutions are handled with high priority.
What is a Promise?
A promise is an object representing the eventual completion or failure of an asynchronous operation. Promises can be in one of three states:
- Pending: The initial state, meaning the operation has not yet completed.
- Fulfilled: The operation completed successfully, and the promise now holds a value.
- Rejected: The operation failed, and the promise holds a reason for the failure.
Once a promise is either fulfilled or rejected, it becomes settled and cannot change states. Promises are designed to be immutable once settled, ensuring predictable behavior in asynchronous code.
Creating a Promise
Promises are created using the Promise
constructor, which takes a function (the executor) as an argument. This executor function receives two parameters, resolve
and reject
, which are used to settle the promise.
Example:
const myPromise = new Promise((resolve, reject) => {
const success = true; // Simulating success or failure
if (success) {
resolve('Operation successful!');
} else {
reject('Operation failed!');
}
});
Explanation:
- In this example, a promise is created that either resolves with a success message or rejects with a failure message based on a condition.
Handling Promises with then
, catch
, and finally
Once a promise is created, you can handle its result using the then
, catch
, and finally
methods:
then(onFulfilled, onRejected)
: Handles the promise’s fulfillment or rejection.catch(onRejected)
: Handles the promise’s rejection (equivalent tothen(null, onRejected)
).finally(onFinally)
: Executes a callback regardless of whether the promise was fulfilled or rejected.
Example:
myPromise
.then((result) => {
console.log(result); // "Operation successful!"
})
.catch((error) => {
console.error(error);
})
.finally(() => {
console.log('Promise has been settled.');
});
Explanation:
- The
then
method handles the resolved value,catch
handles errors, andfinally
executes after the promise is settled, regardless of the outcome.
Promises and the Microtask Queue
When a promise is resolved or rejected, the corresponding handlers (attached via then
, catch
, or finally
) are placed in the microtask queue. This ensures that they are executed immediately after the current synchronous code, but before any macrotasks from the callback queue.
Example of Promises in the Microtask Queue:
console.log('Start');
Promise.resolve('Promise resolved').then((result) => {
console.log(result);
});
console.log('End');
Explanation:
- The promise is resolved immediately, but its
then
handler is placed in the microtask queue. - The synchronous code (“Start” and “End”) is executed first.
- The promise’s
then
handler is executed afterward, even though the promise resolved almost instantly. - The output will be:
Start
End
Promise resolved
This demonstrates how promise callbacks are prioritized and handled promptly by the microtask queue.
Chaining Promises
Promises can be chained together to handle sequences of asynchronous operations, where the output of one promise is passed as input to the next.
Example:
Promise.resolve(1)
.then((value) => {
console.log(value); // 1
return value + 1;
})
.then((value) => {
console.log(value); // 2
return value + 1;
})
.then((value) => {
console.log(value); // 3
});
Explanation:
- Each
then
method returns a new promise, allowing chaining. - The result of one
then
is passed as an argument to the next. - This pattern allows for linear and readable asynchronous code.
Error Handling with Promises
Handling errors in promises is crucial for writing robust asynchronous code. Errors can be caught and managed using the catch
method.
Example:
Promise.resolve(1)
.then((value) => {
console.log(value); // 1
throw new Error('Something went wrong!');
})
.then((value) => {
console.log(value); // This won't execute
})
.catch((error) => {
console.error(error.message); // "Something went wrong!"
});
Explanation:
- If an error is thrown in a
then
handler, it is caught by the nextcatch
handler in the chain. - This error handling mechanism allows you to manage exceptions gracefully in asynchronous operations.
Using Promise.all
and Promise.race
JavaScript provides utility methods like Promise.all
and Promise.race
for handling multiple promises concurrently:
Promise.all
: Waits for all promises to settle and returns an array of their results. If any promise is rejected,Promise.all
rejects with that error.Promise.race
: Returns the result of the first promise to settle, whether fulfilled or rejected.
Example of Promise.all
:
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]).then((values) => {
console.log(values); // [1, 2, 3]
});
Example of Promise.race
:
Promise.race([
new Promise((resolve) => setTimeout(() => resolve('First'), 1000)),
new Promise((resolve) => setTimeout(() => resolve('Second'), 500))
]).then((value) => {
console.log(value); // "Second"
});
Explanation:
Promise.all
waits for all promises to resolve and then returns their results as an array.Promise.race
returns the result of the fastest promise.
Summary:
Promises are a powerful tool for managing asynchronous operations in JavaScript, and they interact closely with the microtask queue to ensure prompt execution of promise handlers. By understanding how promises work and how they are processed in the microtask queue, you can write more efficient and predictable asynchronous code.