Promise states
A Promise can be in one of three states:Pending
The initial state, where the asynchronous operation is still running.
Fulfilled
The operation completed successfully, and the Promise is now resolved with a value.
Rejected
The operation failed, and the Promise is settled with a reason (usually an error).
Basic syntax of a Promise
One of the most common ways to create a Promise is using thenew Promise() constructor. The constructor takes a function with two parameters: resolve and reject. These functions are used to transition the Promise from the pending state to either fulfilled or rejected.
If an error is thrown inside the executor function, the Promise will be rejected with that error. The return value of the executor function is ignored: only resolve or reject should be used to settle the Promise.
- If the
successcondition istrue, the Promise is fulfilled and the value'Operation was successful!'is passed to theresolvefunction. - If the
successcondition isfalse, the Promise is rejected and the error'Something went wrong.'is passed to therejectfunction.
Handling Promises with .then(), .catch(), and .finally()
Once a Promise is created, you can handle the outcome by using the .then(), .catch(), and .finally() methods.
.then()is used to handle a fulfilled Promise and access its result..catch()is used to handle a rejected Promise and catch any errors that may occur..finally()is used to handle a settled Promise, regardless of whether the Promise resolved or rejected.
Chaining Promises
One of the great features of Promises is that they allow you to chain multiple asynchronous operations together. When you chain Promises, each.then() block waits for the previous one to complete before it runs.
Using async/await with Promises
One of the best ways to work with Promises in modern JavaScript is using async/await. This allows you to write asynchronous code that looks synchronous, making it much easier to read and maintain.asyncis used to define a function that returns a Promise.awaitis used inside anasyncfunction to pause execution until a Promise settles.
performTasks function, the await keyword ensures that each Promise is settled before moving on to the next statement. This leads to a more linear and readable flow of asynchronous code.
Essentially, the code above will execute the same as if the user wrote:
Top-level await
When using ECMAScript Modules, the module itself is treated as a top-level scope that supports asynchronous operations natively. This means that you can useawait at the top level without needing an async function.
Async/await can be much more intricate than the simple examples provided. James Snell, a member of the Node.js Technical Steering Committee, has an in-depth presentation that explores the complexities of Promises and async/await.
Promise-based Node.js APIs
Node.js provides Promise-based versions of many of its core APIs, especially in cases where asynchronous operations were traditionally handled with callbacks. This makes it easier to work with Node.js APIs and Promises, and reduces the risk of “callback hell.” For example, thefs (file system) module has a Promise-based API under fs.promises:
- CJS
fs.readFile() returns a Promise, which we handle using async/await syntax to read the contents of a file asynchronously.
Advanced Promise methods
JavaScript’sPromise global provides several powerful methods that help manage multiple asynchronous tasks more effectively:
Promise.all()
This method accepts an array of Promises and returns a new Promise that resolves once all the Promises are fulfilled. If any Promise is rejected, Promise.all() will immediately reject. However, even if rejection occurs, the Promises continue to execute.
Promise.allSettled()
This method waits for all promises to either resolve or reject and returns an array of objects that describe the outcome of each Promise.
Promise.all(), Promise.allSettled() does not short-circuit on failure. It waits for all promises to settle, even if some reject. This provides better error handling for batch operations, where you may want to know the status of all tasks, regardless of failure.
Promise.race()
This method resolves or rejects as soon as the first Promise settles, whether it resolves or rejects. Regardless of which promise settles first, all promises are fully executed.
Promise.any()
This method resolves as soon as one of the Promises resolves. If all promises are rejected, it will reject with an AggregateError.
Promise.reject() and Promise.resolve()
These methods create a rejected or resolved Promise directly.
Promise.try()
Promise.try() is a method that executes a given function, whether it’s synchronous or asynchronous, and wraps the result in a promise. If the function throws an error or returns a rejected promise, Promise.try() will return a rejected promise. If the function completes successfully, the returned promise will be fulfilled with its value.
This can be particularly useful for starting promise chains in a consistent way, especially when working with code that might throw errors synchronously.
Promise.try() ensures that if mightThrow() throws an error, it will be caught in the .catch() block, making it easier to handle both sync and async errors in one place.
Promise.withResolvers()
This method creates a new promise along with its associated resolve and reject functions, and returns them in a convenient object. This is used, for example, when you need to create a promise but resolve or reject it later from outside the executor function.
Promise.withResolvers() gives you full control over when and how the promise is resolved or rejected, without needing to define the executor function inline. This pattern is commonly used in event-driven programming, timeouts, or when integrating with non-promise-based APIs.
Error handling with Promises
Handling errors in Promises ensures your application behaves correctly in case of unexpected situations. You can use.catch() to handle any errors or rejections that occur during the execution of Promises:
async/await, you can use a try/catch block to catch and handle errors:
Scheduling tasks in the event loop
In addition to Promises, Node.js provides several other mechanisms for scheduling tasks in the event loop.queueMicrotask()
queueMicrotask() is used to schedule a microtask, which is a lightweight task that runs after the currently executing script but before any other I/O events or timers. Microtasks include tasks like Promise resolutions and other asynchronous operations that are prioritized over regular tasks.
process.nextTick()
process.nextTick() is used to schedule a callback to be executed immediately after the current operation completes. This is useful for situations where you want to ensure that a callback is executed as soon as possible, but still after the current execution context.
setImmediate()
setImmediate() schedules a callback to be executed in the check phase of the Node.js event loop, which runs after the poll phase, where most I/O callbacks are processed.
When to use each
queueMicrotask()
For tasks that need to run immediately after the current script and before any I/O or timer callbacks, typically for Promise resolutions.
process.nextTick()
For tasks that should execute before any I/O events, often useful for deferring operations or handling errors synchronously.
setImmediate()
For tasks that should run after the poll phase, once most I/O callbacks have been processed.