Clean Error Handling
Start with Try-catch
- Try-catch blocks are like transactions.
- Catch leaves the program in a consistent state, no matter what happens before
Use TDD to Force Exceptions
- Then, add behavior to handlers to satisfy tests
- Like this, you enforce a style of ‘what-could-possibly-go-wrong’ - writing
Wrap Third Party APIs
- If you use an external library or class, make sure to write wrapper functions for each method or function
- This makes you less dependant on third party’s design choices
- And less dependant on a library itself
Don’t Return Null
- If there are several checks for null, there’s no way to figure which one caused the error
- One missing null check and the app might break for good
- Much rather, return an empty list or a special object
Use Special Error Objects
One approach could be to always return a simple tuple with a result and an error. Such as [result, error] - the TryCatchTuple
This approach works in sync, as well as in async code.
Caution: Boilerplate
This approach makes sense if there’s plenty of error messages to be handled. tryCatch comes with its perks at the cost of additional boilerplate
type TryCatchTuple<T> = [T | null, Error | null];
const tryCatch = <T>(fn: () => T, ): TryCatchTuple<T> => {
try {
const result = fn()
return [result, null];
} catch (error) {
return [null, error];
}
};
const tryCatchAsync = async <T>(fn: () => Promise<T>, ): Promise<TryCatchTuple<T>> => {
try {
const result = await fn()
return [result, null];
} catch (error) {
return [null, error];
}
}
const sum = (a: number, b: number, c: number) => {
return a + b + c;
};
const trySum = () => {
const a: number = 1;
const b: number = 2
const c: number = 3
const [myResult, myError] = tryCatch<number>(() => sum(1, b, c));
console.log(myResult, myError)
};
trySum();