Promises in JavaScript
24 min read·Jan 1, 2025
In JavaScript, a promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value.
Promises are generally used to handle long-running or parallel operations without blocking the Node.js process, especially when those operations involve waiting on I/O, such as reading and writing files, performing database queries, making HTTP calls to other services, and so on.
Promises can only be in one of three states:
- Pending: The initial state, neither fulfilled nor rejected.
- Fulfilled: The operation completed successfully.
- Rejected: The operation failed.
Create promises
To create a new promise, you can instantiate the global built-in Promise class by passing an executor function to the constructor:
new Promise((resolve, reject) => {
if (success) {
resolve(value?);
} else {
reject(value?);
}
});
Where:
resolve(value?)is a function used to mark the promise as fulfilled and set its optional argument as its result.reject(value?)is a function used to mark the promise as rejected and set its optional argument as its reason for failure.
As a shorthand, you can also use the Promise.resolve() and Promise.reject() static methods to return a fulfilled or rejected promise without the overhead of instantiating a new Promise object:
Promise.resolve(value?);
Promise.reject(value?);
Note: Any error thrown in the executor function will result in the promise being rejected, similar to calling
reject(error).
Example
Let's consider this function that executes a callback after a random delay between 10ms and 1000ms, and returns a Promise that resolves with the callback's result or rejects if it throws.
function executeWithDelay(callback, min = 100, max = 1000) {
return (...args) => new Promise((resolve, reject) => {
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
setTimeout(() => {
try {
resolve(callback(...args));
} catch(error) {
reject(error);
}
}, delay);
});
}
Handling promises
The success or failure of promises is handled using a logic similar to a try...catch...finally block through chainable methods, such as then(), catch(), and finally():
promise
.then(result => {
// fulfilled
})
.catch(error => {
// rejected
})
.finally(() => {
// settled
});
Where:
-
then()is used to handle the result of fulfilled promises. -
catch()is used to handle the error of rejected promises. -
finally()is used to execute a piece of code once the promise is settled, regardless of its outcome.
Example
Let's consider this script that simulates the retrieval of a user object from a database:
// Simulate a database storage
const database = {
users: [{
name: 'John Doe',
email: 'jdoe@mail.com'
}]
};
// Return user data based on a user identifier
function getUserData(email) {
console.log('Fetching data...');
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = database.users.find(user => user.email === email) || null;
return user ? resolve(user) : reject(new Error('user_not_found'));
}, 800);
});
}
getUserData('jdoe@mail.com')
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log('Done'));
When executed, it will:
-
Define a function named
getUserthat takes as argument a numerical user identifier, and either returns:-
A resolved promise if the
userIdargument matches theidproperty of any object in thedatabase.usersarray. -
A rejected promise with an error otherwise.
-
-
Execute the
getUserfunction and either output the resolved object using thethen()method, or the rejected error using thecatch()method.
Which will produce this output:
Fetching data...
{ name: 'John Doe', email: 'jdoe@mail.com' }
Done
Note: The function uses the
setTimeout()timer function to simulate the delay between the execution of the function, the connection to the database, and the retrieval of data.
Chaining promises
The "callback hell", also known as the "pyramid of doom", refers to a situation where multiple nested callback functions are used to handle asynchronous operations, leading to code that is difficult to read, maintain, and debug.
This issue is common when trying to execute a series of asynchronous tasks, where each task depends on the completion of the previous one.
When using promises, this problem is avoided by the fact that functions that return a promise can be easily chained by passing them as argument of the then() method using the following syntax:
fn()
.then(fn1)
.then(fn2)
.then(...)
.catch(error);
Example
Let's consider this script that simulates the execution of 2 different requests to a database, each based on the data retrieved by the previous one:
// Simulate a database storage
const database = {
users: [{
id: 12,
name: 'John Doe',
email: 'johndoe@mail.com'
}],
bookings: [{
id: 44,
name: 'Private Lake House',
location: '100 Chem. Plouffe, Mont-Tremblant, QC J8E 1J8, Canada',
from: '2025-01-06',
to: '2025-01-10',
userId: 12
}]
}
// Return a unique user identifier based on an email address
function getUserId(userEmail) {
console.log('Searching users...');
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = database.users.find(user => user.email === userEmail) || null;
if (user) {
resolve(user.id);
} else {
reject(new Error('user_not_found'));
}
}, 800);
});
}
// Return an array of bookings associated with a user identifier
function getUserBookings(userId) {
console.log('Searching bookings...');
return new Promise((resolve, reject) => {
setTimeout(() => {
const bookings = database.bookings.reduce((results, booking) => {
if (booking.userId === userId) {
const { name, location, from, to } = booking;
results.push({ name, location, from, to });
}
return results;
}, []);
if (bookings.length) {
resolve(bookings);
} else {
reject(new Error('bookings_not_found'));
}
}, 800);
});
}
// Execute promise chain
getUserId('johndoe@mail.com')
.then(userId => getUserBookings(userId))
.then(bookings => console.log(bookings))
.catch(error => console.error(error));
When executed, it will:
-
Define a function named
getUserId()that takes as parameter an email address and either returns:-
A resolved promise containing an identifier if the
userEmailargument matches theidproperty of any object in thedatabase.usersarray. -
A rejected promise with an error otherwise.
-
-
Define a function named
getUserBookings()that takes as argument a user identifier and either returns:-
A resolved promise containing an array of objects if the
userIdargument matches theuserIdproperty of any object in thedatabase.bookingsarray. -
A rejected promise with an error otherwise.
-
-
Execute the
getUserId()function, then call the chainedgetUserBookings()function, then call the chainedconsole.log()function, or call theconsole.error()function if any of the functions in the chain return a rejected promise.
Which will produce this output:
Searching users...
Searching bookings...
[
{
name: 'Private Lake House',
location: '100 Chem. Plouffe, Mont-Tremblant, QC J8E 1J8, Canada',
from: '2025-01-06',
to: '2025-01-10'
}
]
Note: The promise chain could have also be written using this shorter syntax:
getUserId('johndoe@mail.com') .then(getUserBookings) .then(console.log) .catch(console.error);
Handling multiple promises
To handle multiple promises at once, you can use any of the all(), any(), and race() static methods of the Promise class, that take as argument an array of promises and return a single promise:
Promise.all([promise, ...])
Promise.any([promise, ...])
Promise.race([promise, ...])
Where:
-
all()resolves when all promises in the array are resolved and rejects immediately if any promise is rejected. -
any()resolves when any one of the promises resolves and rejects only if all promises are rejected. -
race()resolves or rejects as soon as the first promise in the array resolves or rejects. -
[promise, ...]is an array of promises.
Example
Let's consider this script that executes 3 promises with a random delay:
// Generate random delay in milliseconds
function generateDelay() {
const min = 100, max = 800;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Return delayed resolved promise
function createPromise() {
const delay = generateDelay();
console.log(`Creating new promise with ${delay}ms delay`);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Promise resolved with ${delay}ms delay`);
}, delay);
});
}
Promise
.any([createPromise(), createPromise(), createPromise()])
.then(result => console.log(result));
When executed, it will:
- Define a function named
generateDelay()that returns a random number between100and800. - Define a function named
createPromise()that returns a resolved promise after a random timeout generated with thegenerateDelay()function. - Execute the
createPromise()function 3 times within thePromise.any()handler. - Output the value returned by the first resolved promise.
Which will produce this output:
Creating new promise with 185ms delay
Creating new promise with 762ms delay
Creating new promise with 699ms delay
Promise resolved with 185ms delay
Enjoying the courses?
I've made these courses completely free so anyone can learn from them. If they've helped you and you'd like to actively support the work behind BackendBrewery, you can leave a tip:
Support BackendBrewery