When to use Custom Exceptions?
A conversation I recently had on the DDD-CQRS-ES slack channel got me thinking about custom exceptions and when to use them. Up until this point I’ve always considered it best practice to define custom exception instead of using the the standard exceptions supplied
Read MoreCustom exceptions get a bad reputation because they are often used as control flow, thrown to signal every foreseeable branch in a program. The better question is whether a caller can genuinely do something different when a failure occurs. If the answer is no, a plain standard exception usually says everything that needs saying, and the extra type only adds noise to the call stack.
At the same time, domain code often benefits from exception types that carry meaning. When a rule is broken inside a domain model, a named exception tells the story far better than a generic one, and it lets the surrounding application layer decide whether to translate it into a result, a rejection message, or a log entry. The type becomes a small piece of the public vocabulary of the system.
Rarely, an exception should be caught and handled in exactly one place. That is a good sign it deserves its own type, because the handler is the real consumer and the throw site is only the producer. In practice this shows up around boundaries such as persistence, messaging, or third-party integrations, where a failure means something specific and recovery has a specific shape.
The gang of behavioural patterns includes several ways to model failure without throwing at all. Result objects, option wrappers, and validation pipelines make the unhappy path visible in the signature of a method, and they remove the surprise of a mid-flight exception that nobody documented. Choosing not to throw is often the cleaner design, particularly deep inside a domain where the state was already checked.
Talking about this at conferences and on community channels is part of the job. A hallway conversation about custom exceptions can turn into a recorded session on patterns and practice, and the value comes from the disagreement as much as the conclusion. People arrive with different backgrounds and defend different defaults, which is exactly why the topic keeps resurfacing.
The same instinct applies to code review. A pull request that introduces a broad catch block or a new exception hierarchy deserves the same scrutiny as one that changes a database schema. Messy merges, forgotten constructors, and unnecessary abstractions rarely arrive alone, and a careful read of the failure handling often reveals where the design is still unclear.
Versioning matters too. A public exception type is a contract, and changing its name or its constructor signature breaks every caller that catches it. Keeping them minimal, immutable, and free of UI or framework dependencies means the library can evolve without forcing painful migrations on the applications that depend on it.
There is a testing angle as well. Exception types that are specific and well-named make assertions readable, because the test reads as a statement about behaviour rather than about implementation details. A suite full of generic failure assertions hides intent, and when something breaks the first time, the message is the only clue you have about what the author actually expected.
Beyond code, the personal parts of a developer's life shape how all this thinking happens. Climbing and surfing in cold water demand a kind of attention to failure and recovery that mirrors careful software design: prepare, commit, and know when the conditions are against you. Conversation is the bridge between the two, and writing is how the ideas survive long enough to be useful to someone else.