Hi everyone! It is possible that it is here for the post title, and I daresay you have had problems handling errors with async/await
. In this post you will learn a trick to avoid trycatch hell but before, it is neccessary to know a little history.
History of Callback.
Once upon a time, developers had to deal with tasks that took a while, and the way we could check if the task was done was through callback.
Callbacks are nothing more than functions that are executed when a task has been completed, either erroneously or successfully. Until this moment, there is nothing bad. The problem arises when these callbacks in turn execute other functions, and so on. And that's when the macabre happens.
For this reason. Promise are born, as a solution to this problem.
Promises.
The promises are objects that represent the completion of a process, this can be a failure (reject) or a success (resolve). Also, lets add this beauty.
Everything seemed magical, until...
Using async await makes the code more readable, it looks more beautiful, but it has a problem, is that if the promise fails, it will stop the flow of our system, so it is necessary to handle the errors.
But when handling these with trycatch we lose that readability, but don't worry, those are over now my dear friend.
How implemented.
First, we are going to simulate a whole. Let's do it.
We define some interfaces, and add test content.
interface Note {
id: number;
name: string;
}
interface Query {
[key: string]: any;
}
const notes: Note[] = [
{
id: 1,
name: "Megadeth",
},
{
id: 2,
name: "Korn",
},
];
We define some functions.
async function update(id: number, data: Omit<Note, "id">, options: Query): Promise<Note> {
const index: number = notes.findIndex(n => n.id === id);
if (index < 0) throw new Error("Note does not exist");
const updated: Note = { id, ...data };
notes.splice(index, 1, updated);
return updated;
};
async function remove(id: number, options: Query): Promise<Note> {
const index: number = notes.findIndex(n => n.id === id);
if (index < 0) throw new Error("Note does not exist.");
const note: Note = notes[index];
notes.splice(index, 1);
return note;
};
We define our promise handler.
async function promHandler<T>(
prom: Promise<T>
): Promise<[T | null, any]> {
try {
return [await prom, null];
} catch (error) {
return [null, error];
}
}
This function receives a promise as a parameter, then we execute the promise within the trycatch, in order to handle the errors, and we will return an array, in which the first index [0] will be the Response or Result and the second [1] the Error.
Note: You may see a T, this is necessary because we need to know the type of data at all times, they are called generics, if you need to know more, click on the following link: https://www.typescriptlang.org/docs/handbook/2/generics.html
Now, we only consume our handler.
const [updated, err] = await promHandler(
update(1, { name: "Mudvayne" }, {})
);
// updated -> {id: 1, name: "Mudvayne"}
// err -> null
const [removed, error] = await promHandler(remove(4, {}));
// removed -> null
// error -> Error "Does not exist."
Now I ask you, does it look better?
Perfect, we already know how to avoid trycatch hell, but this only using promises, what about synchronous functions?
Handling synchronous functions.
We convert our previous functions to synchronous.
function update(id: number, data: Omit<Note, "id">, options: Query): Note {
// ...
};
function remove(id: number, options: Query): Note {
// ...
};
We define our synchronous function handler.
function funcHandler<T extends any[], K>(
func: (...args: T) => K,
...params: T
): [K | null, any] {
try {
return [func(...params), null];
} catch (error) {
return [null, error];
}
}
Explanation: This function is a bit different than the previous one, since in this one, we have to give it the function (without executing) and the parameters.
This function will have two generics, where T represents the parameters that the function receives, and K the value that it returns. Also, we make use of the spread syntax As we do not know the number of parameters that can reach us, we will make use of these 3 magic points (...) ✨
And now, we carry out the previous process, first index, Result; second indicer, Error. And ready!
We carry out the operations.
const [updated, err] = funcHandler(update, 1, { name: "Mudvayne" }, {});
// updated -> {id: 1, name: "Mudvayne"}
// err -> null
const [removed, error] = funcHandler(remove, 6, {});
// removed -> null
// error -> Error "Does not exist."
Great, we no longer have to struggle to make our code look more readable, and also, we reuse the handles.
You know, if you have something to contribute, a question, an improvement, you can contribute in the comments, and if it has been useful, leave your reaction, that makes me happy.
Follow me on social networks.
- 🎉 Twitter: https://twitter.com/ToSatn2
- 💡 Github: https://github.com/IvanZM123
Top comments (32)
This is over-engineered to me
Actually, this error tuple-pattern is very common in Go.
In terms of error handling, my favorite is rust because it has less overhead. But it's uglier and a little bit more complicated.
Yes, I am a gopher, but do you think over 50% JavaScript developer using Go?
No! Of course not lol.
My point was — If a whole modern language, which is known as an easy-to-read language, uses a pattern like that as standard good practice maybe the pattern is not that over-engineered
It seems like this at first glance, but it will be useful when the use of
trycatch
becomes repetitive, and these functions allow you to always reuse them.It's a great solution, but it can be made even simpler. You will need another if statement to handle errors in your example. Why not just check if the promise is an error instance.
const res = await (promise).catch((e) => e).
If(res instanceof Error) {
// Handle err
}
Mm... I've never thought about it, I like your solution. Thanks!
this exactly how I catch errors, and I was wondering whole time reading article "why is there a problem?" However I like wrapper util :)
You need to catch exceptions on the top level of the function and try not to use the try catch scope until you need to re throw something specific.
With the following approach we are hiding
unhandled
rejections by providing status code 500 (Database errors for example) and only the correct thrown exceptions will be shown as a result of the requester.Done years ago of.js.org
Another exemple : await-to-js that returns the error first to enforce it to be handled :
Was about to point out it all seemed a bit like re-inventing the wheel, considering modules like this already exist.
I personally use and like await-to-js for many use cases.
I like, but is not necessary why is Typescript being used (as long as the developer uses static typing)
There's a simple solution. Whatever you
await
should usually return a promise and thus can be directly caught:Completely pointless refactor exercise IMHO, as now instead of try/catch-ing you need to check each time if a function returned an error or not.
When an exception is thrown it will jump out of all blocks, so there's no need to catch it at the same level. You can set just a single try/catch block on the parent (or even higher) scope to catch all of them.
I usually do
Or I'd just throw an error inside the getUserData function
Thank you for your contribution, my friend.
Personally, I do not like to handle errors that way, I consider them incorrect, I leave you the link so you can take a look about making these exceptions: developer.mozilla.org/docs/Web/Jav...
Now we handle it.
Sure, I actually like the golang pattern because is easy to read.
Also I got a different approach by asking about this.
if (error)
in the code, and it will be even worse.This is too complicated, I usually do this
Because you have to know, promsie comes with try/catch
Starts to look a lot like GoLang, nice
Although I find your handler example to be a bit over complicated.