The Await-to-Js
Handling asynchronous function errors and result more elegantly.
The ES6 standard of JavaScript introduced async-await
to avoid callback hell. Since then, we've been able to write sequential calls to a series of asynchronous functions in a chain and handle errors uniformly at the end of the chain.
asyncFun1()
.then((res1) => {
//scope 1
return asyncFunc2(res1)
})
.then((res2) => {
//scope 2
return asyncFunc3(res2)
})
.then((res3) => {
//scope 3
return asyncFunc4(res3)
})
.....
.catch((err)=>{console.log(err)})
However, there's an issue with this approach: we can't access the result of the previous asynchronous function in the next scope of the chain. For instance, in the example above, we can't access res1
in scope 2 or res2
in scope 3.
So, what if we want to pass res1
to asyncFunc3
instead of res2
, or if we want to pass res2
to asyncFunc4
instead of res3
?
To address this, we introduce the await
mechanism to retrieve results from higher-level asynchronous function calls in the subsequent lower-level ones.
So, our code can be modified as follows:
const res1 = await asyncFun1();
const res2 = await asyncFun2(res1);
const res3 = await asyncFun3(res1);
const res4 = await asyncFun4(res2);
But this introduces another issue: how do we handle errors when awaiting a Promise?
Consider this function:
// The function to calculate a / b.
async function divide(a, b) {
return new Promise((resolve, reject) => {
if (b !== 0) resolve(a / b);
else reject(new Error("The divisor cannot be 0."));
});
}
The standard approach is:
try {
const res = await divide(5, 4);
} catch (err) {
console.log(err);
}
Typically, in JavaScript, we use a try-catch
block around the await statement to catch errors.
However, there's another problem: TypeScript doesn't support indicating the type of the parameter for the reject
function. In other words, TypeScript cannot recognize the type of err
in the above code and treats it as any
. Even if you pass a specific Error
class to reject
, TypeScript still recognizes err
as any
.
If reject
doesn't pass an Error
but instead a string
, like this:
async function divide(a, b) {
return new Promise((resolve, reject) => {
if (b !== 0) resolve(a / b);
// Reject with string rather than Error here.
else reject("The divisor cannot be 0.");
});
}
Then, using try-catch
to catch errors wouldn't be appropriate.
Of course, you can enforce programming standards and conventions in your own projects or libraries to ensure that the parameter passed to reject
is always a "Catchable Error." But what if you're using a third-party library?
There's another scenario: you have to nest each await statement within a try-catch
block every time, which leads to poor code flexibility and readability, making it bulky.
try {
const res1 = await asyncFun1();
} catch (err) {
console.log(err);
}
try {
const res2 = await asyncFun2();
} catch (err) {
console.log(err);
}
try {
const res3 = await asyncFun3();
} catch (err) {
console.log(err);
}
// ......
Let's think of an alternative. Is it possible to create a function that awaits an asynchronous function and returns a tuple, providing the result if resolved and the error if rejected?
That brings us to today's topic: implementing an await-to
function to uniformly obtain both resolved and rejected results without having to use try-catch
every time.
function to(promise) {
return new Promise((resolve) => {
promise.then((res) => resolve([null, res])).catch((err) => resolve([err, null]));
});
}
// Example Usage
const examplePromise = new Promise((resolve, reject) => {
// Simulate the Async operation
setTimeout(() => {
reject(new Error("Something went wrong"));
}, 1000);
});
const [err, res] = await to(examplePromise);
console.log([err, res]); // Now it will return the [Error,null]
// Or if the promise is resolved.
// Example Usage
const examplePromise = new Promise((resolve, reject) => {
// Simulate the Async operation
setTimeout(() => {
resolve("Hello");
}, 1000);
});
const [err, res] = await to(examplePromise);
console.log([err, res]); // Now it will return the [null,"Hello"]
This way, we've implemented await-to
. It's very friendly to annotated error types and the implementation is quite elegant. Of course, this method of handling exceptions resembles GoLang!
There are already packages related to await-to-js
on npm that you can use out of the box.
npm i await-to-js