If you've been coding in JavaScript for a while, you've probably encountered promises.
But do you truly understand them at a senior-level? If not, you might encounter random bugs that take a long time to find the root cause, or you may not know how to compose promises with functional patterns.
Whether you're a beginner or an experienced developer eager to learn some senior secrets, this article is packed with code examples and best practices to help you master promises.
If you're more of a visual learner, check out the YouTube video of this same topic below:
So let's start with ...
A promise is an object representing the eventual completion or failure of an asynchronous operation. Think of it as a placeholder for a future value. Instead of immediately returning the final value, an asynchronous method returns a promise to supply the value at some point in the future.
You can create a promise using the Promise
constructor, which requires an executor function. This executor function takes two arguments:
resolve
: A function to call when the promise is fulfilled.reject
: A function to call when the promise is rejected.Here's how you create a resolving promise:
const resolvingPromise = new Promise(resolve => {
resolve('Hello, world!');
});
You use the new
keyword to create a promise. The constructor takes a callback, called the executor function. This executor function's first parameter is the resolve
function. You pass to it the value that the promise should resolve to when the operation completed successfully, in this case 'Hello, world!'
.
It's crucial to differentiate between the promise constructor and the promise instance. The promise constructor refers to the Promise
class (after the new
keyword), whereas a promise instance is an object created using the Promise
constructor, here resolvingPromise
.
And here's a rejecting promise:
const rejectingPromise = new Promise((_, reject) => {
reject(new Error('Something went wrong'));
});
In the rejectingPromise
, you create a new promise that immediately rejects with an error message. By passing (_, reject)
to the executor function, you focus on the reject function, which you call with an error object. You use an underscore _
to ignore the resolve
parameter in the rejecting promise since you don't need it.
Promises are chainable, allowing you to handle sequences of asynchronous operations in a readable manner. The Promise
object provides three main methods:
then
: Attaches callbacks for the resolution or rejection of the Promise. It returns a new promise, allowing for further chaining.catch
: Attaches a callback for handling any errors or rejections in the promise chain. It also returns a new promise.finally
: Attaches a callback that will be executed regardless of the promise's fate (resolved or rejected). It is used for cleanup actions and also returns a new promise.then
The then
method is used to handle the resolved value of a Promise. It takes in a callback function that receives the resolved value as an argument.
const resolvingPromise = new Promise(resolve => resolve('Hello, world!'));
myPromise.then(result => {
console.log(result); // Output: Hello, world!
});
Create a promise that immediately resolves with 'Hello, world!'
. Next, attach a then
handler to the promise. The then
method receives the resolved value and logs it to the console.
You can also handle rejected promises with then
by using the second parameter.
const rejectingPromise = new Promise((_, reject) =>
reject(new Error('Something went wrong')),
);
rejectingPromise.then(null, error => {
console.error(error); // Output: Error: Something went wrong
});
Create a rejecting promise again.
Now when you call then
, the first parameter is null
because you don't have a fulfillment handler. The second parameter is also a callback function and receives the rejection reason as an argument.
This allows you to handle both fulfillment and rejection in the same then method.
Since the then
method is a higher-order function, you can pass in arguments in point-free style.
const resolvingPromise = new Promise(resolve => resolve('Hello, world!'));
resolvingPromise.then(console.log); // Output: Hello, world!
Create a promise that resolves immediately.
Attach a then
handler to the promise. By passing console.log
directly to then
, you're using point-free style. The resolved value is automatically passed as an argument to console.log
.
catch
The catch method is specifically for handling rejected Promises, making your code cleaner and more readable.
const rejectingPromise = new Promise((_, reject) =>
reject(new Error('Something went wrong')),
);
rejectingPromise.catch(error => {
console.error(error); // Output: Error: Something went wrong
});
Create a rejecting promise and attach a catch
handler to rejectingPromise
. The callback function receives the error object and logs it to the console. This is a more straightforward way to handle errors compared to using the second parameter of then
.
finally
The finally
method executes a callback regardless of whether the Promise was fulfilled or rejected, which is useful for cleanup tasks.
const resolvingPromise = new Promise(resolve => resolve('Hello, world!'));
resolvingPromise
.then((result) => {
console.log(result);
})
.finally(() => {
console.log('Promise settled');
});
Create a resolving promise.
Attach a then
handler to the promise. The callback function receives the resolved value and logs it to the console.
Next, attach a finally
handler to the promise. The finally
block runs after the promise settles, regardless of the outcome.
When you chain promises, there are a few behaviors you should be aware of.
then
Handlers: Throwing an error in a then
handler causes the promise to be rejected, skipping subsequent then
handlers until a catch
is found.catch
handler or the second parameter of then
.then
handlers.Let's explore how promise chaining behaves with some examples.
const trace = message => value => {
console.log(message, value);
return value;
};
new Promise(resolve => resolve(1))
.then(trace('first'))
.then(trace('second'));
Define a curried trace
function that takes a message
and returns another function. This returned function takes a value
, logs the message and value, and then returns the value.
Next, create a promise that immediately resolves with 1
. Attach two then
handlers to the promise. Each then
receives the resolved value 1
, logs it along with a message, and passes it to the next handler.
When you run this code, you'll see:
$ node first-chain-example.js
first 1
second 1
Now, let's see what happens if you throw an error in one of the then
handlers.
const trace = message => value => {
console.log(message, value);
return value;
};
const throwError = error => {
throw new Error(error);
};
new Promise(resolve => resolve(2))
.then(throwError)
.then(trace('never gets logged out'))
.catch(trace('caught'));
Define the trace
function again and a throwError
function that takes a value and throws a new Error
with that value.
Create a promise that resolves immediately with 2
. Attach a then
handler that calls throwError
, which throws an error, causing the promise to be rejected. The next then
handler is skipped because the promise is now in a rejected state. The control moves to the nearest catch
handler.
When you run this code, you'll see:
$ node second-chain-example.js
caught Error: 2
The second then
handler doesn't execute because errors cause the promise chain to skip directly to the next catch
.
After catching an error, you can continue chaining then
handlers.
const trace = message => value => {
console.log(message, value);
return value;
};
new Promise((_, reject) => reject(3))
.then(trace('never gets logged out'))
.catch(x => trace('caught')(x + 1))
.catch(trace('previous catch handles this - nothing gets logged out'))
.then(trace('gets the result from catch'));
Start with a promise that rejects with 3
. Then attach in succession a then
handler, two catch
handlers, and finally another then
handler.
When you run this code, you'll see:
$ node third-chain-example.js
caught 4
gets the result from catch 4
The initial then
handler is skipped because the promise is rejected. The first catch
handler receives the rejection reason 3
, increments it by 1
, logs it, and returns the new value 4
. The promise is now resolved with 4
.
Any subsequent catch
handlers are skipped since the promise is resolved. The next then
handler receives the value 4
.
Notice that you can modify the value passed through the chain in any then
or catch
handler, as you did here by incrementing the value by 1
.
Now, let's do one last complex example with multiple rejections.
const trace = message => value => {
console.log(message, value);
return value;
};
const throwError = error => {
throw new Error(error);
};
new Promise(resolve => resolve(4))
.then(trace('first'))
.catch(trace('nothing to catch - never gets logged out'))
.then(trace('second'))
.then(x => x + 1)
.then(trace("keep in mind you can change what's being passed through"))
.then(throwError)
.then(null, error => trace('caught through second parameter')(error))
.catch(trace('previous .then handles this - nothing gets logged out'))
.then(trace('this is the end'));
Resolve a promise with 4
. Then attach a then
handler to it, a catch
handler, 5 more then
handlers, the last of which uses the second parameter of then
to catch any errors, another catch
handler and finally another then
handler.
When you run this code, you'll see:
$ node fourth-chain-example.js
first 4
second 4
keep in mind you can change what's being passed through 5
caught through second parameter Error: 5
this is the end 5
Here's what's happening step by step:
4
.then
: Logs 'first 4'
and returns 4
.catch
: Skipped because there is no error.then
: Logs 'second 4'
and returns 4
.then
increments the value to 5
.then
logs "keep in mind you can change what's being passed through 5"
and returns 5
.then
throws an error with the value 5
.then
: The subsequent then
catches the error using the second parameter, logs 'caught through second parameter Error: 5'
, and returns 5
.catch
: Skipped because the error has been handled.then
: Logs 'this is the end 5'
.A promise exists in one of three states:
onFulfilled()
function, typically associated with a resolve()
call in the promise's handling code.onRejected()
function, which is generally tied to a reject()
call.Once a promise changes from pending to either fulfilled or rejected, it becomes settled. This means it has been resolved (either fulfilled or rejected) and will not change states again. Any further attempts to resolve or reject a settled promise will have no effect, preserving its immutability.
There is no property that tells you the internal promise state. You can only observe the states of a promise indirectly by using the Promise
constructor and inspecting the promise object.
const pendingPromise = new Promise((resolve, reject) => {
console.log('Promise state:', pendingPromise); // Output: Promise { <pending> }
setTimeout(() => {
resolve('Fulfilled!');
console.log('Promise state after resolve:', pendingPromise); // Output: Promise { 'Fulfilled!' }
}, 1000);
});
pendingPromise.then(value => {
console.log('Resolved with:', value); // Output: Resolved with: Fulfilled!
});
Create pendingPromise
, which is initially in the "pending" state.
Next, log pendingPromise
immediately, which shows Promise { <pending> }
. After 1 second, you resolve the promise with 'Fulfilled!'
.
When you now log pendingPromise
using then
, it shows Promise { 'Fulfilled!' }
after 1 second.
waitFor
You can tie what you've learned together and create a waitFor
function.
const waitFor = (value, ms) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (value) {
return resolve(value);
}
return reject(new Error('Value is falsy'));
}, timeout);
});
waitFor(true, 1000).then(() => console.log('Done!'));
This waitFor
function takes in a value and a timeout. It waits for the timeout. Then, if the value is truthy, the promise resolves. Otherwise, it rejects.
Important: Always use return before resolve and reject inside your if and else blocks. Without return, the function might continue executing and potentially call both resolve and reject, leading to unexpected behavior.
Promises are eager. Eager describes the evaluation behavior of operations. There are two different evaluation strategies: eager and lazy.
Eager evaluation computes values as soon as they are assigned. Lazy evaluation delays the computation until the value is needed.
Here is what that means for promises:
console.log("Before promise");
const myPromise = new Promise(resolve => {
console.log("Inside promise");
resolve("Promise resolved");
});
console.log("After promise");
myPromise.then(console.log);
If you run this code, you'll see the following output:
Before promiseInside promiseAfter promisePromise resolved
Before promise
Inside promise
After promise
Promise resolved
Notice that "Inside promise" is logged right after "Before promise", even before we attach a then handler. This demonstrates the eager nature of Promises.
If you want to see an example for lazy evaluation, check out "JavaScript Generators Explained, But On A Senior-Level".
JavaScript provides several static methods on the Promise object to handle multiple Promises and other scenarios.
Promise.resolve
There is another way to create a promise without a function. You can use two static methods on the Promise
object.
The first one is Promise.resolve
, which you can use to create a promise that immediately resolves with a given value.
const promise1 = new Promise(resolve => resolve('Hello, world!'));
const promise2 = Promise.resolve('Hello, world!');
promise2.then((message) => {
console.log(message); // Output: Hello, world!
});
Here, promise1
and promise2
are equivalent.
Promise.reject
There is also Promise.reject
, which allows you to create a promise that immediately rejects with a given reason.
const promise3 = new Promise( (_, reject) => reject(new Error('Something went wrong')));const promise4 = Promise.reject(new Error('Something went wrong'));promise4.catch(error => { console.error(error); // Output: Error: Something went wrong});
const promise3 = new Promise(
(_, reject) => reject(new Error('Something went wrong'))
);
const promise4 = Promise.reject(new Error('Something went wrong'));
promise4.catch(error => {
console.error(error); // Output: Error: Something went wrong
});
Since the promise rejects now, you need to use catch
(or the second parameter of then
) to get the result. Again, promise3
and promise4
are equivalent.
Promise.all
Waits for all Promises in an iterable to be resolved and returns a new Promise that resolves to an array of their results.
const wait = (ms, value) =>
new Promise(resolve => setTimeout(() => resolve(value), ms));
const promises = [wait(2000, 'Hello'), wait(1000, 'World')];
Promise.all(promises).then(results => {
console.log(results); // Output: ['Hello', 'World']
});
Create a helper function wait
that takes a value and a timeout. It returns a promise that resolves after the timeout.
Next, create an array promises
with two promises. One resolves after 2 seconds and the other after 1 second.
Then, you call Promise.all
with promises
. It returns a new promise that resolves when all promises in the array have resolved. The resolved value is an array of the results of the input promises, in this case ['Hello', 'World']
. Since the first promise is slower and resolves after 2 seconds, it takes two seconds until the promise returned from Promise.all
resolves with the array.
Promise.allSettled
The Promise.allSettled
method waits until all promises have settled, meaning they've either fulfilled or rejected. It returns a new promise that resolves with an array of objects describing the outcome of each promise.
const promise1 = Promise.resolve('Success');
const promise2 = Promise.reject('Error');
Promise.allSettled([promise1, promise2]).then((results) => {
results.forEach((result) => console.log(result));
});
// Output:
// { status: 'fulfilled', value: 'Success' }
// { status: 'rejected', reason: 'Error' }
Create two promises: one that resolves and one that rejects. When you use Promise.allSettled
, it waits for both promises to settle and then logs an array with the results. Each result object contains a status
property indicating whether the promise was fulfilled or rejected, and either a value
or a reason
property with the result.
This method is useful when you want to handle multiple promises and process all outcomes, regardless of whether they succeeded or failed.
Promise.any
The Promise.any
method returns a promise that fulfills as soon as any of the promises in the iterable fulfills. If all promises reject, it rejects with an AggregateError
.
const promise1 = Promise.reject('Error 1');
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Success'));
const promise3 = Promise.reject('Error 3');
Promise.any([promise1, promise2, promise3])
.then((value) => {
console.log(value); // Output: Success
})
.catch((error) => {
console.error(error);
});
Create two rejecting promises and one that resolves after 100 milliseconds. Promise.any
waits for the first promise to fulfill. Since promise2
fulfills after 100 milliseconds, the then
handler logs 'Success'
. If you modify the code so that all promises reject, the catch
block will handle an AggregateError
, indicating that all promises were rejected.
Promise.race
The Promise.race
method takes in an array of promises and returns a promise that settles as soon as any of the promises in the iterable settles (fulfilled or rejected).
const promise1 = new Promise((resolve) => setTimeout(resolve, 500, 'First'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Second'));
const promise3 = new Promise((_, reject) => setTimeout(reject, 50, 'Error'));
Promise.race([promise1, promise2, promise3])
.then((value) => {
console.log('Resolved with:', value);
})
.catch((error) => {
console.error('Rejected with:', error); // Output after 50ms: Rejected with: Error
});
Create two promises that resolve after different timeouts and one that rejects after 50 milliseconds. Since promise3
rejects the fastest, Promise.race
settles immediately with that rejection.
You can adjust the timeouts to see how Promise.race
behaves when the first settled promise fulfills instead.
const promise1 = new Promise((resolve) => setTimeout(resolve, 50, 'First'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 500, 'Second'));
const promise3 = new Promise((_, reject) => setTimeout(reject, 100, 'Error'));
Promise.race([promise1, promise2, promise3])
.then((value) => {
console.log('Resolved with:', value);
})
.catch((error) => {
console.error('Rejected with:', error); // Output after 50ms: Rejected with: Error
});
Change the timeouts so that promise1
resolves after 50 milliseconds and promise2
after 500 milliseconds. And promise3
rejects now after 100 milliseconds. Since promise1
fulfills the fastest, Promise.race
resolves with 'First'
. promise3
still rejects, but using Promise.race
that doesn't matter.
async
/ await
The async
and await
keywords provide a more readable and synchronous-looking way to work with Promises.
async
: Declares an asynchronous function that returns a Promise.await
: Pauses the execution of an async function until a Promise is settled.You can make any function declaration asynchronous by writing the async
keyword before the function keyword. And you can make arrow functions asynchronous by writing the async
keyword before the parentheses of the parameter list.
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
};
fetchData().then((data) => {
console.log(data);
});
Define an async function fetchData
that fetches data from an API. Using await
, you pause execution until the fetch promise resolves. You do the same for the response.json()
call. When you call fetchData
, you can use .then
to handle the returned data because the async
keyword makes the function return a promise.
await
Usually you need to declare functions as async
in order to be able to use await
inside them. But some JavaScript environments allow you to use await
in non-async functions. This feature is called "top-level await
".
const result = await Promise.resolve('Hello, world!');
console.log(result); // Output: Hello, world!
With top-level await
, you can await promises directly. So in this case, you don't need to use then
to log out the result.
You can await
synchronous values like strings, numbers, booleans, and arrays.
async function awaitSynchronous() { const result = await 'Hello, world!'; return result;}awaitSynchronous().then(console.log); // Output: Hello, world!
async function awaitSynchronous() {
const result = await 'Hello, world!';
return result;
}
awaitSynchronous().then(console.log); // Output: Hello, world!
Write a function awaitSynchronous
that awaits a string. Since you're using the async
keyword, the function becomes a promise returning function.
Then you await
the string, which does nothing because all "non-promise" values behave the same as they usually do in synchronous code.
try...catch
When using async
/ await
, you can handle errors using try...catch
, making your asynchronous code cleaner.
const fetchData = async () => {
throw new Error('Network error');
};
const getData = async () => {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error('Error:', error.message); // Output: Error: Network error
}
return 'No error'
};
getData().then(console.log); // Output: No error
Define a function fetchData
that throws a new error. Now you can use getData
in another function called getData
, which wraps the execution of fetchData
in a try...catch
block to handle any potential errors. When you now call getData
, it will still resolve, but log out the error from the catch block in the console before logging out the resolved value through the then
method.
return await
In async functions, you might see return await
before a potentially rejecting promise. While the function would work without await
, using it can help with error handling.
const getNumberWithAwait = async () => {
try {
return await Promise.reject(new Error('Oops!'));
} catch (error) {
console.error('Caught in getNumberWithAwait:', error.message);
}
};
const getNumberWithoutAwait = async () => {
try {
return Promise.reject(new Error('Oops!'));
} catch (error) {
console.error('Caught in getNumberWithoutAwait:', error.message);
}
};
getNumberWithAwait().then((number) => {
console.log('Result with await:', number); // Output: Result with await: undefined
});
getNumberWithoutAwait().then((number) => {
console.log('Result without await:', number);
}).catch((error) => {
console.error('Caught outside:', error.message); // Output: Caught outside: Oops!
});
You define two functions, which both reject with an error.
First, getNumberWithAwait
, which uses await
before the return
keyword.
And second, getNumberWithoutAwait
, which doesn't use await
before the return
keyword.
When you now call both, you'll see that getNumberWithAwait
resolves, while getNumberWithoutAwait
rejects. Using await
before the return
ensures that any errors are caught within the try...catch
block.
Therefore, when you deal with promise returning functions, you can see that there are three ways to create a promise.
const asyncInc1 = x => new Promise(resolve => resolve(x + 1));
const asyncInc2 = x => Promise.resolve(x + 1);
const asyncInc3 = async x => x + 1;
asyncInc1
manually constructs a new Promise and resolves it using resolve
.asyncInc2
uses Promise.resolve
for a more concise and immediate resolution.asyncInc3
defines an async function, which implicitly returns a promise resolving to the result.The three asyncInc
functions are equivalent in terms of their behavior and return values. They all return a promise that resolves with x
incremented by 1.
Notice that asyncInc3
does not use await
inside the function. It still returns a promise because of the async
keyword. As mentioned earlier, the await
keyword has no effect on synchronous values.
The then
method unwraps the value of a promise. This means that if you return a promise from within a then
callback, it will be unwrapped so that the next then
in the chain receives the resolved value, not the promise itself.
const inc = x => x + 1;
const asyncInc = async x => x + 1;
Promise.resolve(20).then(inc).then(console.log); // Output: 21
Promise.resolve(20).then(asyncInc).then(console.log); // Output: 21
Define two functions inc
and asyncInc
.
Whether you call the then
method with a normal function inc
or a "promise lifting" function asyncInc
, which lifts its result into the promise context, the value is unwrapped and passed on to the next then
in the chain. Without this unwrapping you'd expect the second example to return Promise { <pending> }
.
The await
keyword works the same way.
async function example() { const result = await Promise.resolve(20).then(inc); return result;}example().then(console.log); // Output: 21
async function example() {
const result = await Promise.resolve(20).then(inc);
return result;
}
example().then(console.log); // Output: 21
Promise.resolve(20).then(inc)
returns a promise that resolves to 21
. The await
keyword then unwraps that value so that example
returns 21
.
Promises themselves do not have a built-in way to be cancelled once initiated.
Some people try to abuse the Promise.race
method as a cancellation mechanism. This looks something like this:
function fetchData() {
const fetchPromise = fetch('https://api.example.com/data').then(response =>
response.json(),
);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout after 5 seconds')), 5000),
);
return Promise.race([fetchPromise, timeoutPromise]);
}
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));
Promise.race()
is used to race the fetchPromise
against a timeoutPromise
. If the fetch operation does not complete within 5 seconds, the timeoutPromise
rejects, effectively canceling the fetch.
This approach is problematic because it shifts cancellation control away from the function that initially created the promise. This function is typically the ideal place for cleanup tasks like stopping timeouts or releasing memory resources. When cancellation control is external, these important cleanup activities might not be performed properly.
In the code example for aborting promises with race, if the timeout triggers first, there's no way to actually stop the fetch operation from continuing in the background, potentially wasting resources.
However, you can control the underlying asynchronous operations of promises even though promises themselves lack a built-in cancellation mechanism. For example, you can use an AbortController
to abort a fetch request to a server.
const controller = new AbortController();
const signal = controller.signal;
const fetchData = () => {
return fetch('https://api.example.com/data', { signal })
.then((response) => response.json());
};
const dataPromise = fetchData();
setTimeout(() => {
controller.abort();
}, 100);
dataPromise
.then((data) => {
console.log('Data:', data);
})
.catch((error) => {
if (error.name === 'AbortError') {
console.error('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
});
Create an abort controller and a signal.
Use AbortController
to cancel the fetch request after 100 milliseconds, which makes it likely that the server hasn't responded yet. While the promise itself isn't cancelled, the underlying operation is aborted, and the promise rejects with an AbortError
. You can handle this error to know that the operation was intentionally cancelled.
Before Promises, handling asynchronous code often meant dealing with callback hell - nested callbacks that made code hard to read and maintain. Promises provide a cleaner, more robust way to handle asynchronous operations.
function getUser(userId, callback) {
setTimeout(() => {
if (!userId) return callback('Invalid user ID');
callback(null, { id: userId, name: 'John Doe' });
}, 100);
}
function getPosts(user, callback) {
setTimeout(() => {
if (!user) return callback('User not found');
callback(null, [{ id: 1, title: 'Post 1' }]);
}, 100);
}
function getComments(post, callback) {
setTimeout(() => {
if (!post) return callback('Post not found');
callback(null, ['Comment 1', 'Comment 2']);
}, 100);
}
getUser(userId, (error, user) => {
if (error) {
console.error('Error fetching user:', error);
return;
}
getPosts(user, (error, posts) => {
if (error) {
console.error('Error fetching posts:', error);
return;
}
getComments(posts[0], (error, comments) => {
if (error) {
console.error('Error fetching comments:', error);
return;
}
console.log('First comment:', comments[0]);
});
});
});
Create three functions getUser
, getPosts
, and getComments
that simulate asynchronous operations with callbacks.
Now, in order to fetch the data, you have to nest the callbacks. This code becomes messy and hard to follow due to the nested callbacks and repetitive error handling. Developers called this "callback hell" for a reason.
You can clean this up using promises to chain the asynchronous operations in a flat and readable manner.
// ... original callback-based functions ...
const promisify =
fn =>
(...args) =>
new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) {
return reject(error);
}
resolve(result);
});
});
const getUserAsync = promisify(getUser);
const getPostsAsync = promisify(getPosts);
const getCommentsAsync = promisify(getComments);
getUserAsync(userId)
.then(user => getPostsAsync(user))
.then(posts => getCommentsAsync(posts[0]))
.then(comments => {
console.log('First comment:', comments[0]);
})
.catch(error => {
console.error('Error:', error);
});
First, write a promisify
function to convert callback-based functions into promise-returning functions.
Then, use the promisify
function to convert your callback-based functions into promise-returning functions. This allows you to chain them using then
, leading to cleaner and more maintainable code. Error handling is simplified with a single catch block at the end.
asyncPipe
If you've read "Unleash JavaScript's Potential With Functional Programming", you know that you can compose functions together. If not, you want to read that article before continuing with the rest of this article because it assumes you are familiar with function composition.
In that article, you learn about the pipe
function that composes functions in reverse mathematical order.
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
This pipe
function can not be used with promise because promises need the then
method.
But you can write an asyncPipe
function that can compose promises.
const asyncPipe = (...fns) => x => fns.reduce((y, f) => y.then(f), Promise.resolve(x));
const asyncInc = async x => x + 1;
const asyncDouble = async x => x * 2;
const incrementAndDouble = asyncPipe(asyncInc, asyncDouble);
incrementAndDouble(20).then(console.log); // 42
Write a function asyncPipe
that composes multiple promise-returning functions. It takes a list of promise-returning functions and returns a new function that applies them in sequence. When you call incrementAndDouble(20)
, it first increments 20
to 21
and then doubles it to 42
.
But this version of asyncPipe
only works with functions that return promises. What if you also want to compose regular functions together with asynchronous functions?
You can write asyncPipe
using async
/ await
.
const asyncPipe = (...fns) => x => fns.reduce(async (y, f) => f(await y), x);
const asyncInc = async x => x + 1;
const double = x => x * 2;
const asyncSquare = async x => x * x;
const incrementAndDouble = asyncPipe(asyncInc, double, asyncSquare);
incrementAndDouble(20).then(console.log); // 1764
Let's break down the steps in asyncPipe
.
You might be asking yourself why you use await y
instead of await f(y)
in the reducer function inside asyncPipe
. If you refactor asyncPipe
to use await f(y)
, it won't work and the code example above returns NaN
.
The reason for that is that because of async
the accumulator y
always returns a pending promise, so you need to await it. If you don't await it, the first step works because it would return await (asyncInc(20))
, which is 21
. But because of the async
keyword in the reducer function, the accumulator would be a pending promise of 21
. Then in the second step you would call await double(Promise.resolve(21))
, which is NaN
because Promise.resolve(21) * 2
would try to multiply an object - the promise - by two.
Composing promises allows you to chain asynchronous operations in a clean and maintainable way reaping all the benefits of functional programming.
<ReadNext posts={[ { ...unleashJavaScriptsPotential, slug: 'unleash-javascripts-potential-with-functional-programming' }, { ...javascriptGeneratorsExplainedSeniorLevel, slug: 'javascript-generators-explained-senior-level' }, ]} />