TypeScript is a great language. TypeScript takes JavaScript and makes it actually good. If there’s one glaring weakness, it’s the inability to use strongly typed catch blocks. However, this is mostly due to a design flaw in the JavaScript language; in JavaScript, you can throw
anything, not just Error
types.
Justification
Consider the following, completely valid TypeScript code:
It’s easy to see why this can be a problem. One use-case where this is less than ideal is consuming a web API from TypeScript. It’s quite common for non-success HTTP status codes (500 Internal Server Error, 404 Not Found, etc.) to be thrown as an error by API consumer code.
Usage
Let’s consider an example using some utilities from AndcultureCode.JavaScript.Core and AndcultureCode.JavaScript.React, which wrap up API responses and errors in ResultRecord
and ResultErrorRecord
Immutable.js Record
types.
You can see in this example that handling errors natively in TypeScript is… quite sloppy. The “maybe monad” common pattern to more generically handle errors and control flow. Basically, what we want to do is create an abstraction that can strongly type thrown errors to a specified type that you know is likely to be thrown. In our case, we want to be able to handle errors from a strongly typed ResultRecord
with ResultErrorRecord
s inside it.
What if we could take the example above, and represent the same logic but with less code and strong typing in the catch block? In the following example, one of result
or error
will be non-null, but not both.
This pattern gives us a more functional approach to error handling, gives us strongly typed errors, and works really, really nicely when used in combination with React hooks. Let’s take a look at a simple React example using some infrastructure from AndcultureCode.JavaScript.Core, AndcultureCode.JavaScript.React and AndcultureCode.JavaScript.React.Components:
Clean, concise, and strongly typed error handling in just 46 lines of code, including the UI.
Implementation
So how does this fancy-schmancy Do.try
work under the hood? By adding an abstraction on top of regular old Promise
s. Let’s break it down.
First, let’s define some utility types we’re going to need:
Next, let’s take a look at our constructor:
That private constructor
is no mistake. You’ll notice in the previous snippets, usage of this pattern starts with Do.try
; that’s because try
is a static factory method that returns an instance of Do
. The private constructor
can only be called internally to the class, by the try
method. The implementation of try
is very straightforward:
The finally
method is just as straightforward, with one important caveat:
Notice the return value, return this;
This allows for method chaining, i.e. Do.try(workload).catch(catchHandler).finally(finallyHandler);
In this code, catch
and finally
are both called on the same instance of Do
which is returned from Do.try
.
There’s also a getAwaiter
method, which allows us to await
for the result. All we need to do is return the internal promise.
Now let’s get to the interesting part; the catch
method. Inside the catch method, we’re going to type guard the thrown object; if the thrown object is a ResultRecord
instance, we cast it as such and pass it as the catch handler’s first argument; otherwise, it’s some unknown error, so we pass it as the catch handler’s second argument. We also need to cast the promise back to a Promise<TReturnVal>
because of the return type of Promise.catch
, but the promise is still a valid Promise<TReturnVal>
.
And there you have a basic implementation of a “maybe monad”. While the implementation here is an opinionated one, offering strongly typed error handling for ResultRecord
errors, you could easily implement the same thing for virtually any type you want to use to wrap up your errors, just as long as you’re able to implement a type guard for it.
Taking It Further
I think strongly typed error handling speaks enough for itself, but we can take it even further. This pattern enables an extremely powerful utility, and I think it’s the strongest argument for using it: default behavior. We can extend our Do
class to have a global configuration, allowing us to define default behavior which is applied to every instance of Do
across the entire application.
All we need to do is add a static configuration mechanism, and implement a check for our configuration inside the constructor:
So what does it look like to apply default behavior? Let’s contrive an example.
We’re working on a large scale React application, and in order to aid debugging errors during development, we want to always log errors to the console in the development environment. Well, with the configuration mechanism we just added, it becomes trivially easy to add this default behavior. Just open up your index.ts
app entrypoint and add the handler:
You could use the same configuration mechanism to add default behavior to the try
or finally
portions of the call chain as well. Feel free to peruse the full implementation used in production here.
The syntax is quite nice to read and easy to understand at a glace, but with the added bonus of having strongly typed errors, and optional default behavior.
What do you think? Are you going to try “maybe monads” or the Do.try
pattern in your next TypeScript project?