The async function
and await
keyword introduced as part of ECMAScript 2017 do provide very useful syntactic sugar on top of promises. Promise
in itself provides alternative to callbacks when writing asynchronous code. Promises can be chained, its inbuilt methods like all
, any
, and race
helps to manages multiple asynchronous tasks.
Check the following example, here getData
function mimics asynchronous behavior. In real world, you can think it as your data layer, using functions like fetch
or a 3rd party library which still using callbacks for asynchronous programming.
const getData = (n: number) => {
return new Promise<number>((res, rej) => {
if (n === 3) {
rej('Can not use 3.');
return;
}
res(n * n);
});
}
If I have to, fetch data for 2, and based on that response fetch data for 3 and 4, then the code will look like something below.
const check = () => {
getData(2)
.then(x2 => {
console.log(x2);
return getData(3);
})
.then(x3 => {
console.log(x3);
return getData(4);
})
.then(x4 => {
console.log(x4);
}).catch((ex) => { // This is catch handler
console.log('Error occurred : Check with Promise.');
console.log(ex);
});
}
If we used async and await, same code will be more readable and easy to understand.
const check = async () => {
try {
const x2: number = await getData(2);
console.log(x2);
const x3: number = await getData(3);
console.log(x3);
const x4: number = await getData(4);
console.log(x4);
} catch (ex) { // This is catch block
console.log('error occurred : check with async and await.');
console.log(ex);
}
}
Error handling, is still a challenge. If a promise is rejected, then either the catch handler get executed, or an exception will be thrown. With await
keywords, only way to handle rejected promise is try-catch
block.
This may work for some cases, but what if you are fine with errors while loading data for 3 and 4. The catch block does not give a good way to handle control flow. You may end up having separate try-catch
blocks for each await
, and that will worsen the problem.
Languages like go
, has a different philosophy to handle errors. It segregates error
from exception
, and communicate errors with ordinary values as return parameters.
Lets see what happens when we try that philosophy here.
Always keep your promises
Let change the getData function so that it can never reject the promise. The promise will be always resolved, and errors will be reported via a return type.
type PromiseResponse<T> = Promise<[string] | [null, T]>;
const getData = (n: number) : PromiseResponse<number> => {
return new Promise((res) => {
if (n === 3) {
// no reject here
res(['Can not use 3.']);
return;
}
res([null, n * n]);
});
}
I have declared a Type here PromiseResponse
, which is a Promise returning tuple, and will assist TypeScript for better syntax checking.
- First item will be error : string or null.
- Second item will be actual result of Type T or undefined.
const check3 = async () => {
const [e2, x2] = await getDataV2(2);
// Here for TypeScript x2 is either number or undefined
if (x2 === undefined) {
console.log('Error while fetching data for 2');
return;
}
// As x2 is checked for undefined
// at this line x2 is number
console.log(x2);
// now fetch data for 3 and 4
const [e3, x3] = await getDataV2(3);
if (x3 !== undefined) {
console.log(x3);
}
const [e4, x4] = await getDataV2(4);
if (x4 !== undefined) {
console.log(x4);
}
}
With the new approach, code don't need to use try-catch
block and we have better control over flow.
I am using this techniques for application layer, which seats between UI and underlying data, and its make life much easier.
Based on your requirements, you can extend the Type PromiseResponse
into a class, and with helper methods like Success
and Error
to make your code more readable.
I have a utility function, named as aKeptPromise
on propose. With this function getData is more readable.
function aKeptPromise<T>(
callback: (
success: (result: T) => void,
failure: (error: string) => void
) => void
): PromiseResponse<T> {
return new Promise((res) => {
callback(
(r) => res([null, r]),
(e) => res([e])
);
});
}
const getDataV3 = (n: number) : PromiseResponse<number> => {
return aKeptPromise((success, failure) => {
if (n === 3) {
failure('Can not use 3.');
return;
}
success(n * n);
});
}
Thanks for reading. Let me know if you have comments.
Top comments (0)