Mathieu Eveillard

Bug, error, or nominal case: what is the business intent?

Bug, error, or nominal case: what is the business intent?

We’ve seen in the past that it’s important to distinguish between two types of errors:

At this point, a common pitfall would be to never throw exceptions again and to “put everything in Result.” That’s a bad idea, of course. So let’s add some nuance.

To better understand the topic, let’s consider a logistics context and a ShipmentPort that expresses the domain’s persistence requirements, within the framework of a port/adapter architecture (a.k.a., hexagonal architecture).

Let’s now look at these three signatures: getRequiredShipmentById, getShipmentById, and getNextScheduledShipmentAfterDate. We’ll see that they imply very distinct meanings from a business perspective.

  1. getRequiredShipmentById : here, it is expected that the Shipment exists in the database. If it does not, it means that we, as developers, have failed to respect a business invariant—it’s a bug. By definition, this is a case we do not know how to handle, so we should throw an exception (throw). In TypeScript, a function’s signature does not specify the exceptions that the function might throw. We therefore indicate this behavior through the function name: Required, or sometimes OrThrow. Note that we still use the Result type, because there may be other errors—ones that are, in fact, quite predictable (such as the database not responding…).

  2. getShipmentById : here, the shipmentId is derived from user input. The case where no Shipment matches the ID provided by the user is therefore entirely predictable and must be accounted for. This is a functional error, since the application cannot function without Shipment, but it’s a type of error we know how to handle, using the Error subtype of the Result monad.

  3. getNextScheduledShipmentAfterDate : here, talking about an “error” is actually a misnomer, since the absence of an error is part of the nominal scenario—for example, creating the Shipment if it doesn’t already exist. Note here the use of the Either monad (type Either<U> = Some<U> | None), which is a way to represent the absence of Shipment (neater than Shipment | null, which force us to perform multiple checks). “That’s a lot of monads,” you might say, but once you’ve tried them, it’s hard to go back 🙂

One might think that the third case introduces a new type of error. But as we’ve seen, this case is actually a nominal case, so there are only two error cases: those mentioned in the introduction—bugs or predictable errors.

What should we take away from all this? That it is the business logic that determines which case we’re in:

Note: It’s also worth noting that ShipmentId is the encapsulation of an ID with nominal type emulation—an approach that helps avoid many errors related to “primitive obsession.”

← All posts