Javascript Wait for Promise Before Continuing
In this article, you will learn how you can simplify your callback or Promise based Node.js Node.js is an asynchronous event-driven JavaScript runtime and is the most effective when building scalable network applications. Node.js is free of locks, so there's no chance to dead-lock any process. application with async Asynchrony, in software programming, refers to events that occur outside of the primary program flow and methods for dealing with them. External events such as signals or activities prompted by a program that occur at the same time as program execution without causing the program to block and wait for results are examples of this category. Asynchronous input/output is an... functions (async await In an async function, you can await any Promise or catch its rejection cause. In ECMAScript 2017, the async and await keywords were introduced. These features make writing asynchronous code easier and more readable in the long run. They aid in the transition from asynchronicity to synchronism by making it appear more like classic synchronous code, so they're well worth learning. ).
Whether you've looked at async/await and promises in JavaScript before, but haven't quite mastered them yet, or just need a refresher, this article aims to help you.
What are async functions in Node.js?
Async functions are available natively in Node and are denoted by theasync
keyword in their declaration. They always return a promise, even if you don't explicitly write them to do so. Also, theawait
keyword is only available inside async functions at the moment – it cannot be used in the global scope.
In an async function, you can await anyPromise
or catch its rejection cause.
So if you had some logic implemented with promises:
function handler (req, res) { return request('https://user-handler-service') .catch((err) => { logger.error('Http error', err); error.logged = true; throw err; }) .then((response) => Mongo.findOne({ user: response.body.user })) .catch((err) => { !error.logged && logger.error('Mongo error', err); error.logged = true; throw err; }) .then((document) => executeLogic(req, res, document)) .catch((err) => { !error.logged && console.error(err); res.status(500).send(); }); }
You can make it look like synchronous code usingasync/await
:
async function handler (req, res) { let response; try { response = await request('https://user-handler-service') ; } catch (err) { logger.error('Http error', err); return res.status(500).send(); } let document; try { document = await Mongo.findOne({ user: response.body.user }); } catch (err) { logger.error('Mongo error', err); return res.status(500).send(); } executeLogic(document, req, res); }
Currently in Node you get a warning about unhandled promise rejections, so you don't necessarily need to bother with creating a listener. However, it is recommended to crash your app in this case as when you don't handle an error, your app is in an unknown state. This can be done either by using the--unhandled-rejections=strict
CLI flag, or by implementing something like this:
process.on('unhandledRejection', (err) => { console.error(err); process.exit(1); })
Automatic process exit will be added in a future Node release – preparing your code ahead of time for this is not a lot of effort, but will mean that you don't have to worry about it when you next wish to update versions.
Patterns with async functions in JavaScript
There are quite a couple of use cases when the ability to handle asynchronous operations as if they were synchronous comes very handy, as solving them with Promises or callbacks requires the use of complex patterns.
Since node@10.0.0, there is support for async iterators and the related for-await-of loop. These come in handy when the actual values we iterate over, and the end state of the iteration, are not known by the time the iterator method returns – mostly when working with streams. Aside from streams, there are not a lot of constructs that have the async iterator implemented natively, so we'll cover them in another post.
Retry with exponential backoff
Implementing retry logic was pretty clumsy with Promises:
function request(url) { return new Promise((resolve, reject) => { setTimeout(() => { reject(`Network error when trying to reach ${url}`); }, 500); }); } function requestWithRetry(url, retryCount, currentTries = 1) { return new Promise((resolve, reject) => { if (currentTries <= retryCount) { const timeout = (Math.pow(2, currentTries) - 1) * 100; request(url) .then(resolve) .catch((error) => { setTimeout(() => { console.log('Error: ', error); console.log(`Waiting ${timeout} ms`); requestWithRetry(url, retryCount, currentTries + 1); }, timeout); }); } else { console.log('No retries left, giving up.'); reject('No retries left, giving up.'); } }); } requestWithRetry('http://localhost:3000') .then((res) => { console.log(res) }) .catch(err => { console.error(err) });
This would get the job done, but we can rewrite it withasync/await
and make it a lot more simple.
function wait (timeout) { return new Promise((resolve) => { setTimeout(() => { resolve() }, timeout); }); } async function requestWithRetry (url) { const MAX_RETRIES = 10; for (let i = 0; i <= MAX_RETRIES; i++) { try { return await request(url); } catch (err) { const timeout = Math.pow(2, i); console.log('Waiting', timeout, 'ms'); await wait(timeout); console.log('Retrying', err.message, i); } } }
A lot more pleasing to the eye isn't it?
Intermediate values
Not as hideous as the previous example, but if you have a case where 3 asynchronous functions depend on each other the following way, then you have to choose from several ugly solutions.
functionA
returns a Promise, thenfunctionB
needs that value andfunctionC
needs the resolved value of bothfunctionA
's andfunctionB
's Promise.
Solution 1: The.then
Christmas tree
function executeAsyncTask () { return functionA() .then((valueA) => { return functionB(valueA) .then((valueB) => { return functionC(valueA, valueB) }) }) }
With this solution, we getvalueA
from the surrounding closure of the 3rdthen
andvalueB
as the value the previous Promise resolves to. We cannot flatten out the Christmas tree as we would lose the closure andvalueA
would be unavailable forfunctionC
.
Solution 2: Moving to a higher scope
function executeAsyncTask () { let valueA return functionA() .then((v) => { valueA = v return functionB(valueA) }) .then((valueB) => { return functionC(valueA, valueB) }) }
In the Christmas tree, we used a higher scope to makevalueA
available as well. This case works similarly, but now we created the variablevalueA
outside the scope of the.then
-s, so we can assign the value of the first resolved Promise to it.
This one definitely works, flattens the.then
chain and is semantically correct. However, it also opens up ways for new bugs in case the variable namevalueA
is used elsewhere in the function. We also need to use two names —valueA
andv
— for the same value.
Are you looking for help with enterprise-grade Node.js Development?
Hire the Node developers of RisingStack!
Solution 3: The unnecessary array
function executeAsyncTask () { return functionA() .then(valueA => { return Promise.all([valueA, functionB(valueA)]) }) .then(([valueA, valueB]) => { return functionC(valueA, valueB) }) }
There is no other reason forvalueA
to be passed on in an array together with the PromisefunctionB
then to be able to flatten the tree. They might be of completely different types, so there is a high probability of them not belonging to an array at all.
Solution 4: Write a helper function
const converge = (...promises) => (...args) => { let [head, ...tail] = promises if (tail.length) { return head(...args) .then((value) => converge(...tail)(...args.concat([value]))) } else { return head(...args) } } functionA(2) .then((valueA) => converge(functionB, functionC)(valueA))
You can, of course, write a helper function to hide away the context juggling, but it is quite difficult to read, and may not be straightforward to understand for those who are not well versed in functional magic.
By usingasync/await
our problems are magically gone:
async function executeAsyncTask () { const valueA = await functionA(); const valueB = await functionB(valueA); return function3(valueA, valueB); }
Multiple parallel requests with async/await
This is similar to the previous one. In case you want to execute several asynchronous tasks at once and then use their values at different places, you can do it easily withasync/await
:
async function executeParallelAsyncTasks () { const [ valueA, valueB, valueC ] = await Promise.all([ functionA(), functionB(), functionC() ]); doSomethingWith(valueA); doSomethingElseWith(valueB); doAnotherThingWith(valueC); }
As we've seen in the previous example, we would either need to move these values into a higher scope or create a non-semantic array to pass these values on.
Array iteration methods
You can usemap
,filter
andreduce
with async functions, although they behave pretty unintuitively. Try guessing what the following scripts will print to the console:
- map
function asyncThing (value) { return new Promise((resolve) => { setTimeout(() => resolve(value), 100); }); } async function main () { return [1,2,3,4].map(async (value) => { const v = await asyncThing(value); return v * 2; }); } main() .then(v => console.log(v)) .catch(err => console.error(err));
- filter
function asyncThing (value) { return new Promise((resolve) => { setTimeout(() => resolve(value), 100); }); } async function main () { return [1,2,3,4].filter(async (value) => { const v = await asyncThing(value); return v % 2 === 0; }); } main() .then(v => console.log(v)) .catch(err => console.error(err));
- reduce
function asyncThing (value) { return new Promise((resolve) => { setTimeout(() => resolve(value), 100); }); } async function main () { return [1,2,3,4].reduce(async (acc, value) => { return await acc + await asyncThing(value); }, Promise.resolve(0)); } main() .then(v => console.log(v)) .catch(err => console.error(err));
Solutions:
-
[ Promise { <pending> }, Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
-
[ 1, 2, 3, 4 ]
-
10
If you log the returned values of the iteratee with map
you will see the array we expect:[ 2, 4, 6, 8 ]
. The only problem is that each value is wrapped in a Promise by theAsyncFunction
.
So if you want to get your values, you'll need to unwrap them by passing the returned array to aPromise.all
:
main() .then(v => Promise.all(v)) .then(v => console.log(v)) .catch(err => console.error(err));
Originally, you would first wait for all your promises to resolve and then map over the values:
function main () { return Promise.all([1,2,3,4].map((value) => asyncThing(value))); } main() .then(values => values.map((value) => value * 2)) .then(v => console.log(v)) .catch(err => console.error(err));
This seems a bit more simple, doesn't it?
Theasync/await
version can still be useful if you have some long running synchronous logic in your iteratee and another long-running async task.
This way you can start calculating as soon as you have the first value – you don't have to wait for all the Promises to be resolved to run your computations. Even though the results will still be wrapped in Promises, those are resolved a lot faster then if you did it the sequential way.
What aboutfilter
? Something is clearly wrong…
Well, you guessed it: even though the returned values are[ false, true, false, true ]
, they will be wrapped in promises, which are truthy, so you'll get back all the values from the original array. Unfortunately, all you can do to fix this is to resolve all the values and then filter them.
Reducing is pretty straightforward. Bear in mind though that you need to wrap the initial value intoPromise.resolve
, as the returned accumulator will be wrapped as well and has to beawait
-ed.
.. As it is pretty clearly intended to be used for imperative code styles.
To make your.then
chains more "pure" looking, you can use Ramda'spipeP
andcomposeP
functions.
Rewriting callback-based Node.js applications
Async functions return aPromise
by default, so you can rewrite any callback based function to use Promises, thenawait
their resolution. You can use theutil.promisify
function in Node.js to turn callback-based functions to return a Promise-based ones.
Rewriting Promise-based applications
Simple.then
chains can be upgraded in a pretty straightforward way, so you can move to usingasync/await
right away.
function asyncTask () { return functionA() .then((valueA) => functionB(valueA)) .then((valueB) => functionC(valueB)) .then((valueC) => functionD(valueC)) .catch((err) => logger.error(err)) }
will turn into
async function asyncTask () { try { const valueA = await functionA(); const valueB = await functionB(valueA); const valueC = await functionC(valueB); return await functionD(valueC); } catch (err) { logger.error(err); } }
Rewriting Node.js apps with async await
- If you liked the good old concepts of
if-else
conditionals andfor/while
loops, - if you believe that a
try-catch
block is the way errors are meant to be handled,
you will have a great time rewriting your services usingasync/await
.
As we have seen, it can make several patterns a lot easier to code and read, so it is definitely more suitable in several cases thanPromise.then()
chains. However, if you are caught up in the functional programming craze of the past years, you might wanna pass on this language feature.
Are you already usingasync/await
in production, or you plan on never touching it? Let's discuss it in the comments below.
Are you looking for help with enterprise-grade Node.js Development?
Hire the Node developers of RisingStack!
jeanhaticappons49.blogspot.com
Source: https://blog.risingstack.com/mastering-async-await-in-nodejs/
0 Response to "Javascript Wait for Promise Before Continuing"
Post a Comment