The topic of custom exceptions is surprisingly controversial. Some argue that exceptions shouldn't be used for anything related to "business", while others (me included) say that exceptions like InsufficientFunds are fine - I even wrote a whole chapter about using custom exceptions to simplify controllers in my Rails Refactoring book.
I've read an interesting blog post today about custom exceptions (here - unfortunately it's in Polish), where the author advocates for using "business exceptions".
The example comes from a dialling application, so there are the following exceptions:
When I first looked at the code, I nodded in agreement. Given such domain, those exceptions make total sense to me. I'd implement it the same way.
When I linked to this post in our team chat, Mirek (who has more knowledge about DDD and CQRS than I do) said that it'd be better to implement with domain events. This surprised me a bit, as when the topic of custom exceptions is brought up, domain events are rarely shown as an alternative.
What's the difference between custom exceptions and domain events?
Look at this code:
# CUSTOM EXCEPTIONS class CallAlreadyInProgress < StandardError end class Dialer def call(number) raise CallAlreadyInProgress.new if call_already_in_progress? end end dialer = Dialer.new begin dialer.call(number) rescue CallAlreadyInProgress ui_notifier.send_message("Call already in progress") end # with domain events class CallAlreadyInProgress < Event end class ConnectionEstablished < Event end class Dialer def call(number) if call_already_in_progress? CallAlreadyInProgress.new.publish() return end ConnectionEstablished.new.publish() end end # those two can be in different places of our project event_bus.register(CallAlreadyInProgress, ui_notifier.send_message("Call already in progress")) dialer.call(number)
In the code, it means that when you call dialler.call(number), you're not directly interested in the result, even to the point, that the method can fail (raise an exception). You made a command and that's it.
Now, obviously, you do need to know about the failure. That's why you publish an event, using whatever mechanism under the hood. It can be a simple singleton EventBus class, which just keeps a map of objects interested in certain events and notifies them if any such event happens. In our case, we could have a UINotifier class listening to the CallAlreadyInProgress event and sending a special UI message to the user of the system (the technical details are not important here - it can be via polling or Web Sockets).
There is another difference - with events we need to "publish" the fact that all went fine. What was implicit with exceptions (no exception is raised - success), here needs to be explicit. We publish the "ConnectionEstablished" event.
This creates a nice simplification of the whole code around it. It may make it a bit more indirect, but it's actually very simple. All of the pieces of code involved do just one thing.