Thoughts on Await and Try…Catch

Tak Sampson
Jan 7, 2019 · 10 min read
Photo by Markus Spiske on Unsplash

Using Async Functions with BAM!

Asynchronous JavaScript is an indispensable tool for developers and software engineers at all levels. This article will cover the basic idea of await and try...catch along with some helpful and possibly counterintuitive illustrations. Finally we’ll take a look at a more practical real-world example of asynchronous code in action within the source code of the lightweight serverless framework BAM! (For those who are curious the full white paper for the framework can be found here.)

What are Await and Try…Catch?

Async Functions

Before diving into await and try...catch, a brief bit of background on async functions. While it might take a bit of getting used to, async functions are indispensable for building web apps and are a necessary part of any developer’s arsenal.

async function foo() {
return ‘hello world’;
}
function bar() {
return ‘hello world’;
}

Await

This is where await comes in. await must itself be used within the scope of an async function. It behaves slightly differently depending on the expression that it is passed; either it is passed an object that is already a Promise, or if the value of the expression is not a Promise, it is converted to a Promise that is resolved to that value. Either way we now have some Promise, and await pauses the execution of the surrounding async function until said Promise is either resolved or rejected. The value of the entire await expression will be the value of the fulfilled Promise.

let a;
let b;
let c;
let d;
async function foo() {
// do stuff that might take a while
return 3;
}
(async () => {
a = await 1;
b = await Promise.resolve(2);
c = await foo();
d = foo();
})();

Some More Subtleties

Speaking of small details, let’s take a look at some tricks and subtleties concerning await.

const delay = (time) => {
return new Promise(resolve => setTimeout(resolve, time));
}
(async () => {
console.log(‘hello…’);
await delay(2000);
console.log(‘ world!’);
})();
(async () => {
await console.log(‘foo’);
await console.log(‘bar’);
await console.log(‘baz’);
await console.log(‘qux’);
})();
foo
bar
baz
qux
console.log(‘foo’);
console.log(‘bar’);
console.log(‘baz’);
console.log(‘qux’);
console.log(1);(async () => {
await delay(200);
await console.log(2);
await console.log(3);
await console.log(4);
})();
console.log(5);

Try…Catch

All of the above examples involve a Promise or sequence of Promises that eventually all resolve, which does raise the question: what if one of them is rejected instead? This is where try...catch comes into play. Without try...catch let’s say you have something like this:

const foo = async () => {
throw ‘some error’;
};
(async () => {
await console.log(1);
await console.log(2);
await foo();
await console.log(3);
await console.log(4);
})();
const foo = async () => {
throw ‘some error’;
};
(async () => {
try {
await console.log(1);
await console.log(2);
await foo();
await console.log(3);
await console.log(4);
} catch (err) {
console.log(err);
}
})();
1
2
some error

BAM!

One tricky detail when working with try...catch blocks comes up when those blocks are nested; in fact our team encountered this quite often building BAM!, and so we’ll briefly discuss a practical case below.

// omitted for brevity
try {
const asyncFuncParams = [resourceName, path, roleName, deployDir];
const lambdaData = await bamBam(deployLambda, { asyncFuncParams });
if (lambdaData) await writeLambda(lambdaData, path);
if (deployLambdaOnly) {
await deleteStagingDirForLambda(resourceName, path);
return;
}
const {
restApiId,
endpoint,
methodPermissionIds,
} = await deployApi(resourceName, path, httpMethods, stage);
const writeParams = [
endpoint,
methodPermissionIds,
resourceName,
restApiId,
path,
];
if (restApiId) await writeApi(…writeParams);
await deleteStagingDirForLambda(resourceName, path);
} catch (err) {
bamError(err);
}
module.exports = async function bamBam(asyncFunc, {
asyncFuncParams = [],
retryError = 'InvalidParameterValueException',
interval = 3000,
retryCounter = 0,
} = {}) {
const withIncrementedCounter = () => (
{
asyncFuncParams,
retryError,
interval,
retryCounter: retryCounter + 1,
}
);
const retry = async (errCode) => {
if (firstTooManyRequestsException(errCode, retryCounter)){
logAwsDelayMsg();
}
const optionalParamsObj = withIncrementedCounter();
await delay(interval);
const data = await bamBam(asyncFunc, optionalParamsObj);
return data;
};
try {
const data = await asyncFunc(...asyncFuncParams);
return data;
} catch (err) {
const errorCode = err.code;
if (errorCode === retryError) {
const data = await retry(errorCode);
return data;
}
throw (err);
}
};

Conclusion

Phew! Hopefully by now you can appreciate some of the intricacies of working with asynchronous code. This is definitely an area that is rife with tiny pitfalls where it is very easy to make mistakes that are both potentially frustrating and difficult to spot. However, it can be tremendously rewarding to finally grasp these concepts, and it is well worth the effort to learn. Happy coding!

bam-lambda

a lightweight serverless framework for humans

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store