Do Not Catch Exceptions in TypeScript
The problem with exceptions is that they are not type-safe
Example. We are writing a function getEmailProvider(email)
which has three possible outcomes:
- The email provider is supported
- The email provider is not supported
- Email is not valid
Using return value for the successful outcome 1. and exceptions for the error outcomes 2. and 3. looks like a good idea.
We defined a custom exception for each error outcome by extending the built-in Error
object. This is not straightforward in TypeScript but there is an NPM package ts-custom-error available to help.
The code that is calling getEmailProvider()
wraps it in try..catch
and handles each known exception with instanceof
.
Here comes the problem with exceptions. They are not part of the getEmailProvider()
function signature. Each time we use the function, we have to look into its source code to learn about all the possible exceptions we should handle. This becomes nearly impossible to track if the function is using other functions that can throw. We could use a JSDoc annotation @throws
but it will get out of date — guaranteed.
Result Type
Now let’s rewrite this code using result type instead.
We wrapped the successful and error outcomes into one type— EmailProviderResult
and we are always returning it.
The code that is calling getEmailProvider()
handles all known outcomes with conditional statements. Our IDE is helping us with suggestions. The compilation fails if we use an invalid error code.
Interestingly although this code is type-safe, it is simpler. There is only one level of nesting while the example with exceptions has two. The variable result
is available in the entire block while with exceptions it is available only inside the try
block. These look like small details but can play a big role in the overall code cleanliness.
If this article convinces you that result types are the right approach, look into some of the existing generic solutions — neverthrow, ts-results or @usefultools/monads to name a few.
Do We Still Need Exceptions?
Yes. Exceptions are a great way to quickly terminate the application at any depth level if there is no way to continue. The built-in Error
exception is good enough for this.
Ideally, there should be one catch
in your application— The handler that renders generic 500 Oops! error page or API response.
Conclusion
Throw an exception when you want to alarm the person that’s an on-call duty for your application. Return result type for everything else.
Be prepared to refactor what once used to be clean code. The exception vs result decision changes over time with the context where your code is used.