Errors
Example code: src/lib/errors
But what if something goes wrong?
In the case of our example code, simulating a library of books, loaning a book can throw all kinds of exceptions:
the customer might not yet have a library card
the book being scanned might not belong to the library
the customer might already have reached their loan limit
the customer might have outstanding fines
et cetera
When this happens, what we'd like is for our application to intercept the error and respond appropriately to the user, e.g. with a human readable error message.
To do this we need a set of Error classes.
Which errors should I define in my lib?
Only errors that are to do with the domain itself - either invalid operations or entities in invalid state. Errors thrown by things in context should be defined in the adapter layer.
The point is that your lib/adapters
module represents all the errors that can be thrown by the library.
Extending JavaScript errors
In JavaScript you can extend the native Error
class to include additional properties and customise the message property. This can help you pass useful information to the user and any error reporting system you have in place.
Let's look at a custom error in our Library project:
You'll notice several things:
Firstly, the invalidOperationErr property is there so that downsteam code knows it can report the error to the user, who has just attempted something impossible. You always want to opt in to making errors public, as unfiltered errors can reveal a lot of information to malicious users.
Secondly, the userId property is there so that any logging or reporting can capture it.
And finally, there's a custom message, formatted with the user's ID. This can be used as a more human readable error message.
What fields and customisations you make are entirely up to you. None are mandated by this architecture. The important thing is to think carefully about what reporting you need and how exceptions should be presented to the user. Once you know that, the question of which fields you need will answer itself.
Using assertions (and errors) to narrow typescript types
One new feature as of TypeScript 3.7 is the asserts
keyword. This lets you tag a function as asserting than an input has a particular type.
This is really helpful when a context function can return a type or null, but to be valid it really has to be defined. For instance:
In this example, the user store can return a user or null. But we don't want to log a loan by a 'null' user. By calling assertUser
we can both throw an error in this case, and tell TypeScript that they type of user
has gone from User | null
to just User
. This means you won't have to do any annoying casts or !
operators.
Assertions let you transform a (validValue | invalidValue) -> (validValue | throw err)
An aside: assertions beyond null checks
Imagine you have a system for managing magazine subscriptions, with an operation to upgradeSubscription
. How do you handle someone upgrading a subscription that's been cancelled?
One way is to encode two types:
And write an assertion:
Now we can use our assertion and type system to make illegal upgrades impossible to represent without causing a TypeScript error:
Using types to express not just that a given data structure is correct, but that its semantics are correct, is called using invariants.
Putting it together: a practical example
Now we can use all these ideas to write the full 'loan book' operation:
The function now checks the operation is valid, and throws typed, reportable errors if the user attempts to make a nonsensical loan.
Assertion functions mean that we can e.g. get the
book.id
without TypeScript complaining that the book could benull
.We can use the
invalidOperationErr
property to tell whether an error is coming from the operation (reportable) or the context (not to be reported).
Last updated