Let's Clean Up: Ugly Try-Catches!
We've all been there. We have all used await
on async
methods, forgetting to wrap them in try-catch
just to be told off about an Unhandled Error
😱
But it's not just these aysnc
methods that might throw. Perhaps we use a third-party library that throws, or we design our codebase in such a way that we throw errors intentionally to bubble error handling up a few layers.
So we proceed by wrapping our calls to the throwable methods in our try-catch
blocks.
Perfect! 😍
🤔 Or is it?
Working in a code base where you can expect methods to throw
can lead to situations where your logic is wrapped in try-catch
blocks. It also leads to other code design problems.
Take a look at the examples below:
try {
const myVar = myThrowableMethod();
} catch (err) {
console.log(err);
}
// Wait, there's an error here:
console.log(myVar);
// myVar doesn't exist outside of the try scope.
As we can see above, the myVar
variable doesn't exist outside of the try
block. But we need that value to continue our logic!!
So now we need to do something a little different.
How about:
try {
const myVar = myThrowableMethod();
const newVar = manipulateMyVar(myVar);
const response = sendApiRequest(newVar);
return respponse;
} catch (err) {
console.log(err);
}
🤮!
No. Let's not do this.
Now all our logic wrapped inside the try
block. That's ugly.
Also, if any of the subsequent method calls throw, are we sure we want to handle them the same way!? Possibly, but most likely not!
Ok, let's try something else...
let myVar;
try {
myVar = myThrowableMethod();
return respponse;
} catch (err) {
console.log(err);
}
const newVar = manipulateMyVar(myVar);
const response = sendApiRequest(newVar);
This is a little better, but still not perfect. It's still ugly as myVar
has to be declared then initialised almost immediately after just in a different scope. It also presents an issue when myThrowableMethod
does throw, as execution will continue and try to use myVar
!
🐛 Alert!
I could keep going, giving more situations where these try-catch
blocks can present code design, readability, and maintainability problems.
Instead, I'll present you with a solution!
The Solution 🚀
I wrote a small library to tackle this issue head on :
Let's welcome no-try to the scene. 🎉
What is no-try
? 😱
no-try
is a tiny library that takes the try-catch
out of your code, improves the readability and maintainability of your code whilst helping to improve code design.
It exposes two functions. noTry
and noTryAsync
with the latter resolving and returning the result of Promises.
Don't believe me? Let's look at it in more detail.
To install it, simply run npm i --save no-try
Then add it to your file:
In TypeScript;
import { noTry } from "no-try";
In JS:
const noTry = require("no-try").noTry;
Now, let's refactor our example above to use no-try
.
const { result, error } = noTry(() => myThrowableMethod());
if (error) {
// Handle error
return;
}
const newVar = manipulateMyVar(result);
const response = sendApiRequest(newVar);
🎉🎉🎉
Isn't that cleaner!?
If you have a standard error handling function, you can supply that to noTry
and it will invoke it for you if an error occurs!
function myCustomErrHandler(error) {
// Do something to handle error
}
const { result, error } = noTry(() => myThrowableMethod(), myCustomErrHandler);
if (error) {
return;
}
const newVar = manipulateMyVar(result);
const response = sendApiRequest(newVar);
And that's it!
We've removed the try-catch
blocks from our code, preventing issues relating to block-scoped variables, whilst also allowing our code to be much more readable without sacrificing the flexibility of handling the error how we want.
You can read more on no-try
over on GitHub.
Now go clean your code!
If you have any questions, feel free to ask below or reach out to me on Twitter: @FerryColum.
Top comments (13)
Imho, no. That's adding a useless dependency to a problem that's easily solved by adding
return
statement in the catch block. Could probably benefit from using more functional programming to refactor that bit but that depends purely on what other stuff is going on before this block.Agreed. You can add the return to the catch to break execution in the method, but you're still left with what I feel is less readable code. I guess it comes down to a matter of preference! :)
A lot of coding is down to preference. :)
Well sometimes less Magic and more control is better
The
no-try
lib should still provide you with all the control you need :)It just removes the need to write the try catch blocks manually. It still uses them under the hood.
Good code should read like a document, really good code can be understood by a non coder.
There are sometimes benefits from abstracting complex functionality, but it is a balance of readability, functionality and performance.
Code is first and foremost a document describing the behaviour of your action.
Looking at it from a different perspective, if a Dev was to come along and they had never seen this code or your no-try mechanism.
They have to read your code, then they have to go and learn your no-try and maybe they'll understand it, maybe they won't.
However there is a high probability they've seen a try-catch, so they don't need to learn this functionality and so immediately understand what you are trying to achieve!
This is an over abstraction which makes you feel better as a particular function has been reduced, but as a piece of code actually over complicates the readability of the code as it deviates from normal coding practice.
Try catch is easily explained to a non-coder, your function less so, especially as the name doesn't reflect the functionality of it's operation.
This is why function naming is critical in abstraction.
I understand what you're saying, and it makes sense. There's a cost to learning what the method does, but the same can be said for any dependency you add to your codebase.
Take for example someone adding lodash, are using both
_.clone
and_.cloneDeep
but someone else comes along to read it. If they don't know about lodash, or the difference between those functions, they will have to go and look them up to.Perhaps simply calling the function
tryCatch
might have explained more what the function is doing, BUT, for any dev that wants it to be called this, then they can alias the import.import { noTry as tryCatch } from 'no-try';
Or
const tryCatch = require('no-try').noTry;
There is already another solution that was created years ago, he took approach to handle errors same way as in GO
github.com/scopsy/await-to-js
Anyway good job mate !
Thanks!
Awesome link, there is something similar to the above called await-of that also handles promises, I wanted a method that also worked for standard methods that throw errors.
Ugly isn't really a professional adjective to use.
A chunk of code can be simple or complex, fast or slow, stateful or stateless, among others measurable metrics.
Saying a code is "clean" or "ugly" is really subjective, it's an opinion. For example, a try/catch isn't intrinsically ulgy nor bad by itself.
Every instruction in a codebase is a technical choice made in a certain context that must be understood. A hackathon blockchain explorer will have a different codebase than a battle-tested and historical ERP.
It's pretty reductive for our field to use such adjectives.
I don't wanna be rude, but the JavaScript ecosystem doesn't need a new (26 lines of code) package while receiving a "go clean your code" with it.
I know there is a lot of new developers on this platform, and this is awesome! But this is not the kind of message we need to send them.
Agree: it is subjective.
Disagree: JS ecosystem doesn't need a new package (whatever the reasoning behind it is).
Without developers trying to think of new ways to make their own and others lives maybe just that little bit easier, then we would never innovate. We wouldn't get new technologies.
If a dev, at any level, has an idea for how to make their life easier, we should be at least encouraging them to investigate it, not telling them to sit on it just because it may not be needed, (which is also subjective).
Is native XHR API bad or ugly? Subjective, but we still have Fetch, Axios, Angular Http etc.
So yes, it's subjective. If you don't mind try catches, still use them, if you don't like how they look, or have been burned with bugs caused or relating to their usage, then there is an alternative.
Question, shouldn't try catch be used at the beginning of writing your code and after that you clean it up, because you know now that the code is stable?
I'm not sure I fully understand what you are saying.
It sounds like you're saying you wrap the full app startup in a try catch and then come along later and remove it?
Try catches should be wrapped around any method you expect can throw an error at the layer you wish to handle it.
Perhaps you have three layers:
UI
Business
Data Layer
Your data layer may throw and error but you want to catch it at the UI level so you can show an error to the user.
Or perhaps, your business layer throws an error, but you want to catch it immediately so you can call a fallback method etc.