throw anything, not just
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.
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
ResultErrorRecords 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
error will be non-null, but not both.
Clean, concise, and strongly typed error handling in just 46 lines of code, including the UI.
First, let’s define some utility types we’re going to need:
Next, let’s take a look at our constructor:
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
private constructor can only be called internally to the class, by the
try method. The implementation of
try is very straightforward:
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,
finally are both called on the same instance of
Do which is returned from
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
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
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?
Code @ andculture
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or…
Common patterns, functions, etc... used when building react applications This package is installed via npm or yarn npm…