Promises guide on Nodejs perspective.
This repository is designed to help developers understand and apply Promises effectively, helping you to develop your APIs
and avoid common problems.
- Introduction
- Promise Constructor
- Instance Methods
- Async and Await
- Static Methods
- For await of
- CPU-Intensive VS I/O Operations
- REFS
- TO-DO
Promises are a foundational feature of modern JavaScript designed to simplify handling asynchronous operations. They represent a placeholder for a value that will be available at some point in the future, either as a result of a successful operation (fulfilled) or a failure (rejected). Unlike callback-based approaches, Promises provide a structured and predictable way to manage asynchronous flows, avoiding common pitfalls like callback hell.
Promises have three states:
- Pending: The initial state, where the operation has neither succeeded nor failed.
- Fulfilled: The operation completed successfully, and the resulting value is available.
- Rejected: The operation failed, and the reason for the failure is provided.
The Promise
constructor is used to create a new Promise object. The MDN Docs says "it is primarily used to wrap callback-based APIs that do not already support promises".
So, it takes a single argument, a function (commonly referred to as the executor function), which is executed immediately upon the promise’s creation. The executor function receives two arguments:
- resolve: A function used to mark the promise as fulfilled and provide a result.
- reject: A function used to mark the promise as rejected and provide a reason for failure.
It’s common to see people using new Promise
as a tool to transform anything into a Promise. You shouldn't. I suggest using new Promise
the way MDN suggests.
The setTimeout
function is a classic example of a callback-based asynchronous operation. Using the Promise
constructor, we can transform it into a Promise for more manageable asynchronous workflows:
const delay = (ms) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Resolved after ${ms}ms`);
}, ms);
});
};
delay(1000)
.then((result) => console.log(result)) // Logs: Resolved after 1000ms
.catch((error) => console.error(error)); // Handles any errors
We are going to use the upper sintax a lot in this doc. The fact is that this is an easy way to emulate a promise operation.. So when reading.. New Promise of Set Timeout Fn can be any Promise you want to resolve. Sometimes we are going to use Promise.resolve too with a similar purpose.
Promises provide several methods that allow chaining and handling asynchronous operations. These methods are part of the Promise
prototype and operate on specific promise instances. Key methods include:
The .then
method is used to define what happens when a promise is fulfilled. It receives a callback function as its argument, which is executed with the value of the resolved promise.
const successPromise = Promise.resolve('Success');
successPromise
.then((value) => console.log(value)) // Logs: Success
.then(() => console.log('Chaining works!')); // Logs: Chaining works!
The .catch
method is used to handle errors or rejections. It catches any error that occurs in the promise chain.
const errorPromise = Promise.reject('Something went wrong');
errorPromise
.catch((error) => console.error(error)); // Logs: Something went wrong
The .finally
method executes a callback function once the promise settles, regardless of whether it was fulfilled or rejected. This is often used for cleanup operations.
const conditionalPromise = new Promise((resolve, reject) => {
const condition = true;
condition ? resolve('Resolved') : reject('Rejected');
});
conditionalPromise
.then((value) => console.log(value))
.catch((error) => console.error(error))
.finally(() => console.log('Promise settled')); // Always logs: Promise settled
The .then
method schedules its callback to run asynchronously. This can lead to unexpected behavior where the execution of code after .then
does not wait for the promise to resolve.
console.log('Start');
Promise.resolve('Operation with .then')
.then((value) => {
console.log(value); // Logs: Operation with .then
});
console.log('End');
/**Output
Start
End
Operation with .then
*/
The .then
method schedules its callback to run in the microtask queue, which is processed after the current synchronous code execution completes. This means that console.log('End') runs before the .then callback executes.
The microtask queue is a high-priority task queue in the JavaScript event loop. It is where microtasks are stored, which include tasks like:
Resolving or rejecting Promises. Calling .then, .catch, and .finally handlers. Operations from APIs such as queueMicrotask. Microtasks are executed immediately after the currently executing JavaScript code completes and before any tasks in the macrotask queue (e.g., setTimeout or setImmediate) are processed. This behavior ensures that Promise handlers run as soon as possible, but still asynchronously.
The tricky behavior of .then
callbacks scheduling their execution in the microtask queue can often be solved by using async/await
. This syntax provides a more synchronous-like flow for handling asynchronous operations, making the code easier to read and debug.
async function main() {
console.log('Start');
const result = await new Promise((resolve) => {
setTimeout(() => resolve('Operation completed'), 1000);
});
console.log(result); // Logs: Operation completed
console.log('End');
}
main();
When await is used, it also relies on the microtask queue to resume execution. However, unlike .then, await "pauses"(will be better explained in the next topic) the execution of the entire function it is in until the Promise resolves. This makes the flow appear more synchronous and easier to follow.
async
and await
are modern JavaScript features introduced in ES2017 (ES8). They provide a cleaner and more intuitive way to work with Promises, allowing asynchronous code to be written in a synchronous-like manner. This is particularly useful in Node.js for handling asynchronous workflows such as file I/O, API requests, and database queries.
-
async
Functions: Declaring a function as async means it will always return a Promise, even if the function body does not explicitly return one. -
await
Keyword: Inside an async function, await acts as syntax sugar that allows writing asynchronous code in a sequential style. It instructs the JavaScript engine to wait for a Promise to resolve or reject, restructuring the function so that the remaining code is executed as a continuation (callback) once the Promise settles. Importantly, this process does not block the event loop, allowing other tasks to proceed concurrently.
.then
:
Adds the provided callback to the microtask queue, which will be executed after the current synchronous code finishes.
async
and await
Similar to .then, it schedules the continuation of the function in the microtask queue after the awaited Promise settles.
Unlike .then, it restructures the function to appear synchronous, effectively postponing the execution of subsequent code within the async function until the Promise resolves or rejects.
So you dont need to use things like Promise.resolve
to create a promise mock. You can just use async functions.
A subtle but important difference exists between return and return await inside async functions. While both ultimately resolve a Promise, returning without await loses the stack trace context. This can make debugging harder when dealing with complex asynchronous workflows.
TLDR: Use await in every layer you are coding
dont skip await on your functions
returnWithAwait().catch(console.log) // will have returnWithAwait in the stacktrace
returnWithoutAwait().catch(console.log) // // will not have returnWithoutAwait in the stacktrace
async function returnWithAwait() {
return await throwAsync('with await')
}
async function returnWithoutAwait () {
return throwAsync('without await')
}
async function throwAsync(msg) {
await null // need to await at least something to be truly async :)
throw new Error(msg)
}
ref: goldbergyoni/nodebestpractices#737
When using multiple await
statements in an async
function, they are executed sequentially by default. This means each await
waits for the previous Promise to resolve before the next one starts.
async function fetchDataSequentially() {
console.log('Start fetching data');
const data1 = await new Promise((resolve) =>
setTimeout(() => resolve('Data 1'), 1000)
);
console.log(data1); // Logs: Data 1
const data2 = await new Promise((resolve) =>
setTimeout(() => resolve('Data 2'), 1000)
);
console.log(data2); // Logs: Data 2
console.log('Finished fetching data');
}
fetchDataSequentially();
/**Output
Start fetching data
Data 1
Data 2
Finished fetching data - this will take 2 seconds
*/
Each await statement "pauses" the execution of the async function until the Promise resolves. This creates a serial execution where each Promise is resolved one after the other, even if they are independent.
When using for...of
loops with await
, each iteration is executed serially. This means the loop waits for the current iteration's Promise to resolve before moving to the next iteration.
async function fetchSequentiallyWithForLoop() {
console.log('Start fetching');
const tasks = ['Task 1', 'Task 2', 'Task 3'];
for (const task of tasks) {
const result = await new Promise((resolve) =>
setTimeout(() => resolve(task), 1000)
);
console.log(result); // Logs each task in order, with a delay of 1 second per task - 3
}
console.log('Finished fetching');
}
fetchSequentiallyWithForLoop();
/** Output:
Start fetching
Task 1
Task 2
Task 3
Finished fetching
*/
The behavior of the for...of loop with await can change depending on how you handle errors. Below are the two common approaches:
In the fail fast approach, if any Promise in the sequence rejects, the loop stops immediately, and the error is propagated. This is the default behavior of for...of with await.
async function failFast() {
console.log('Start fetching');
const tasks = ['Task 1', 'Task 2', 'Task 3'];
try {
for (const task of tasks) {
// Simulate async task creation with potential failure
const result = await new Promise((resolve, reject) => {
if (task === 'Task 2') {
reject('Error in Task 2'); // Simulate an error for Task 2
}
setTimeout(() => resolve(task), 1000); // Simulate async task
});
// Log success
console.log(result); // Logs Task 1, then stops at Task 2
}
} catch (error) {
console.error('Error caught:', error); // Logs: Error caught: Error in Task 2
return; // Early exit from the loop on error
}
console.log('Finished fetching'); // This only logs if there are no errors
}
failFast();
/** Output:
Start fetching
Task 1
Error caught: Error in Task 2
*/
The loop stops as soon as the Promise for Task 2 rejects. The error is caught in the try...catch block, but the subsequent tasks (Task 3) are not executed.
In the fail safe approach, you ensure that the loop continues execution even if one of the Promises rejects. This is done by wrapping each await in a try...catch block.
async function failSafe() {
console.log('Start fetching');
const tasks = ['Task 1', 'Task 2', 'Task 3'];
for (const task of tasks) {
try {
const result = await new Promise((resolve, reject) => {
if (task === 'Task 2') {
reject('Error in Task 2'); // Simula erro na Task 2
}
setTimeout(() => resolve(task), 1000); // Simula uma tarefa assíncrona
});
console.log(result); // Logs o resultado da tarefa
} catch (error) {
console.error('Error handled locally:', error); // Logs: Error handled localmente
}
}
console.log('Finished fetching');
}
failSafe();
/** Output:
Start fetching
Task 1
Error handled locally: Error in Task 2
Task 3
Finished fetching
*/
Each await is individually wrapped in a try...catch block, so errors in one Promise do not affect the others. The loop continues execution for all tasks, logging errors only for the ones that fail.
When you initialize multiple Promises upfront and then await
them later, they are resolved simultaneous, even if you use await
sequentially. This is because the Promises start executing as soon as they are created, regardless of when await
is used.
function tenSecondsPromise() {
return new Promise((resolve) =>
setTimeout(() => resolve('10 seconds'), 10000)
);
}
function twoSecondsPromise() {
return new Promise((resolve) =>
setTimeout(() => resolve('2 seconds'), 2000)
);
}
function oneSecondPromise() {
return new Promise((resolve) =>
setTimeout(() => resolve('1 second'), 1000)
);
}
async function executePromisesSimultaneously() {
console.time('executePromisesSimultaneously');
// Start all Promises simultaneously
const promise1 = tenSecondsPromise();
const promise2 = twoSecondsPromise();
const promise3 = oneSecondPromise();
// Await them sequentially
console.log(await promise1); // Logs: 10 seconds
console.log(await promise2); // Logs: 2 seconds
console.log(await promise3); // Logs: 1 second
console.timeEnd('executePromisesSimultaneously'); // Logs: ~10 seconds
}
executePromisesSimultaneously();
In this example the log will come on the correct order bc of the await nature. But time time will be 10 secs, since you initialized the promises before await for them.
When working with Promises in combination with array methods like .map, .forEach, or .filter, it's common to run into issues if the Promise resolution is not properly managed. These methods do not inherently handle asynchronous operations, which can lead to unexpected behavior.
Array methods are not async or promise aware
refs(if you google it you will find tons, i just did it):
async function asyncMap(array, callback) {
const results = [];
for (let i = 0; i < array.length; i++) {
const result = await callback(array[i], i, array); // Sequentially await each callback
results.push(result);
}
return results;
}
- Definition:
Promise.all
takes an iterable of Promises and returns a single Promise that resolves when all of the input Promises are fulfilled. If any Promise rejects, the returned Promise immediately rejects with the reason of the first rejection. - Use Case: Use when you need all tasks to succeed to proceed.
- Promise all works with the concept of Fail Fast
const promises = [
Promise.resolve('Result 1'),
Promise.resolve('Result 2'),
Promise.reject('Error 1'),
];
Promise.all(promises)
.then((results) => console.log('All Results:', results))
.catch((error) => console.error('Caught Error:', error)); // Logs: Caught Error: Error 1
- Definition: Promise.allSettled takes an iterable of Promises and returns a Promise that resolves once all input Promises settle (fulfilled or rejected). It provides the status and value/reason for each Promise.
- Use Case: when you need to process results regardless of success or failure.
- Promise allSettled works with the concept of Fail Safe, and errors as Values
const promises = [
Promise.resolve('Result 1'),
Promise.reject('Error 1'),
Promise.resolve('Result 2'),
];
Promise.allSettled(promises).then((results) => {
console.log('All Settled:', results);
/** Output:
[
{ status: 'fulfilled', value: 'Result 1' },
{ status: 'rejected', reason: 'Error 1' },
{ status: 'fulfilled', value: 'Result 2' }
]
*/
});
- Definition:
Promise.race
takes an iterable of Promises and returns a single Promise that resolves or rejects as soon as the first input Promise settles (either fulfills or rejects). - Use Case: Use when you need the result of the fastest task.
const promises = [
new Promise((resolve) => setTimeout(() => resolve('Fast Success'), 500)),
new Promise((resolve) => setTimeout(() => resolve('Slow Success'), 2000)),
new Promise((_, reject) => setTimeout(() => reject('Fast Error'), 1000)),
];
Promise.race(promises)
.then((result) => console.log('First Resolved:', result)) // Logs: First Resolved: Fast Success
.catch((error) => console.error('First Rejected:', error));
- Definition:
Promise.any
takes an iterable of Promises and returns a Promise that resolves as soon as any of the input Promises is fulfilled. If all Promises reject, it rejects with an AggregateError.. - Use Case: Use when you need the result of the fastest task.
const promises = [
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.resolve('First Success'),
];
Promise.any(promises)
.then((result) => console.log('First Success:', result)) // Logs: First Success: First Success
.catch((error) => console.error('All Rejected:', error)); // Logs: AggregateError: All Promises were rejected
- Definition:
Promise.resolve
is a static method that returns a Promise object that is resolved with the given value. If the value is already a Promise, it simply returns that Promise unchanged. - Use Case:
Promise.resolve
when you need to wrap a synchronous value or non-Promise object into a resolved Promise.
const existingPromise = Promise.resolve('Already resolved');
const promise = Promise.resolve(existingPromise);
promise.then((result) => {
console.log(result); // Logs: Already resolved
});
Personally i just use if for mocks
- Definition:
Promise.reject
is a static method that returns a Promise object that is rejected with the specified reason.. - Use Case:
Promise.reject
to create a Promise that is immediately rejected, often for testing or error-handling scenarios.
const errorPromise = Promise.reject('Something went wrong');
errorPromise.catch((error) => {
console.error(error); // Logs: Something went wrong
});
Personally i just use if for mocks
for await...of shines when working with asynchronous data streams, such as reading data chunks from a file or API. Lots of people use this in the for of await use case... Actually, it wont have value. You should use for await of in async data streams.
const { Readable } = require('stream');
// Create a readable stream with async iteration
const stream = Readable.from(['Chunk 1', 'Chunk 2', 'Chunk 3'], { objectMode: true });
async function processStream() {
for await (const chunk of stream) {
console.log(`Processing: ${chunk}`);
}
console.log('Stream processing complete');
}
processStream();
/** Output:
Processing: Chunk 1
Processing: Chunk 2
Processing: Chunk 3
Stream processing complete
*/
-
for...of { await }
In this approach, you iterate over an array of Promises using a for...of loop and explicitly use await within the loop body. Each await "pauses" the loop execution until the current Promise resolves. -
for await of
This approach is specifically designed to work with asynchronous iterables, where each iteration automatically awaits the Promise. It simplifies handling asynchronous streams or dynamic data sources
Mixing CPU-bound tasks (like data processing or transformations) with I/O-bound tasks (like reading/writing files or API calls) in the same block of code often leads to unorganized and hard-to-read code.
By separating these steps, your code becomes cleaner, easier to understand, and easier to debug. Think of it as a natural flow:
Get the data (I/O). Process the data (CPU). Save the data (I/O). When you mix these operations together, it’s like cooking a meal while washing the dishes and reorganizing the kitchen at the same time—it might work for one meal, but it's chaos when scaling up! Keep your tasks structured, and future you (and your team) will thank you.
What not to do
const fs = require('fs').promises;
async function processFiles(filePaths) {
for (const filePath of filePaths) {
const fileData = await fs.readFile(filePath, 'utf-8'); // I/O: leitura
console.log(`Arquivo lido: ${filePath}`);
const processedData = fileData
.toUpperCase() // CPU: transformação
.split('\n')
.join(', ');
await fs.writeFile(`processed_${filePath}`, processedData); // I/O: escrita
console.log(`Arquivo processado salvo: processed_${filePath}`);
}
}
processFiles(['file1.txt', 'file2.txt']);
what to start doing
const fs = require('fs').promises;
async function readFiles(filePaths) {
const fileContents = await Promise.all(
filePaths.map((filePath) => fs.readFile(filePath, 'utf-8'))
);
return fileContents;
}
function processContents(fileContents) {
const processedContents = fileContents.map((content) =>
content.toUpperCase().split('\n').join(', ')
);
return processedContents;
}
async function saveFiles(filePaths, processedContents) {
await Promise.all(
filePaths.map((filePath, index) =>
fs.writeFile(`processed_${filePath}`, processedContents[index])
)
);
}
//Facade
async function processFiles(filePaths) {
const fileContents = await readFiles(filePaths);
const processedContents = processContents(fileContents);
await saveFiles(filePaths, processedContents);
}
processFiles(['file1.txt', 'file2.txt']);
- MDN Docs (https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Promise)
- Nodejs Docs (https://nodejs.org/docs/latest/api/)
- A "Lab" Repo a did last Year (https://github.com/samsantosb/Javascript-Promises)
- And github/stackoverflow discussions i mentioned
- Generators
- Cool Abstractions with promises
- Batching