I am a bit troubled between the use of guard clauses vs error handling via `try` `except` and `finally`.
I understand that when writing a function, and trying to rule out edge-cases, we would like to start with ruling out the negative outcomes first via if statements. Then, place the return value outside of the if-else statements. This gives a much easier and scalable function and therefore a good practice.
However, I see that python - and even other languages - have error handling ( try, except, finally). I understand that these may be used on a piece of code to catch any exceptions, like TypeErrors, ValueErrors, or DivideByZero errors.
However, I don’t understand how they differ from guard clauses? Are there any situation where they cannot be used interchangeably (by just re-formatting the code)?
Guard clauses are usually used to make sure your parameters are valid and error handling is used to catch errors in the rest of the code, especially where you might expect a failure (i.e. fetching from an API, connecting to a database, etc.).
The main difference with error handling, is that you can catch unintended errors along with intended ones. With guard clauses, you only can catch what you expect to catch.
This also extends to nested errors throw, if you call function2() from within function1() and function2() calls function3() which throws, you can catch this error at function1() level, where-as with guard clauses, you’d have to catch this at function2() level, then have a guard clause at the parent function as well.
Finally, throwing an error can be built upon guard clauses throwing specifics “types” of errors, such as what you gave. So not only can you handle unintended errors, but you can handle intended ones within the correct part of the code.
This isn’t related to guard clauses, but just functions in general. There’s a concept called “early return” where you check for values earlier in the function and return if you find it. This usually results in eliminating else statements entirely and preventing nesting. This also means the further down the function you go, the more “niche” your code is, as by the end of the function you have bypassed all the previous “checks”.
Here’s an example using JS, throwing custom class errors:
function validateUser(user) {
if (!user.email) {
// the user is also returned for further context as to what failed
throw new ValidationError("Email is required.", { user });
}
if (user.password.length < 8) {
throw new ValidationError("Password must be 8+ characters.", { user });
}
// this doesn't matter much, as any validation failure would result in an error thrown, tossing the parent caller into their own `catch` block with the `error` passed being the custom ValidationError.
return true;
}