5 Reasons Why Business Exceptions Are a Bad Idea

Table Of Contents

I recently had a conversation about exception handling. I argued that business exceptions are a good thing because they clearly mark the possible failures of a business method. If a rule is violated, the business method throws a “business” exception that the client has to handle. If it’s a checked exception, the business rule is even made apparent in the method signature - at least the cases in which it fails.

My counterpart argued that failing business rules shouldn’t be exceptions because of multiple reasons. Having thought about it a bit more, I came to the conclusion that he was right. And I came up with even more reasons than he enumerated during our discussion.

Read on to find out what distinguishes a business exception from a technical exception and why technical exceptions are the only true exceptions.

Technical Exceptions

Let’s start with technical exceptions. These exceptions are thrown when something goes wrong that we cannot fix and usually cannot respond to in any sensible way.

An example is Java’s built-in IllegalArgumentException. If someone provides an argument to a method that does not follow the contract of that method, the method may throw an IllegalArgumentException.

When we call a method and get an IllegalArgumentException thrown into our face, what can we do about it?

We can only fix the code.

It’s a programming error. If the illegal argument value comes from a user, it should have been validated earlier and an error message provided to the user. If the illegal argument comes from somewhere else in the code, we have to fix it there. In any case, someone screwed up somewhere else.

A technical exception is usually derived from Java’s RuntimeException, meaning that it doesn’t have to be declared in a method signature.

Business Exceptions

Now, what’s a business exception?

A business exception is thrown when a business rule within our application is violated:

class Rocket {

  private int fuel;

  void takeOff() throws NotEnoughFuelException {
    if (this.fuel < 50) {
      throw new NotEnoughFuelException();
    }
    lockDoors();
    igniteThrusters();
  }
  
}

In this example, the Rocket only takes off if it has enough fuel. If it doesn’t have enough fuel, it throws an exception with the very imaginative name of NotEnoughFuelException.

It’s up to the client of the above code to make sure that the business rule (providing at least 50 units of fuel before takeoff) is satisfied. If the business rule is violated, the client has to to handle the exception (for example by filling the fuel tank and then trying again).

Now that we’re on the same page about technical and business exceptions, let’s look at the reasons why business exceptions are a bad idea.

#1: Exceptions Should not be an Expected Outcome

First of all, just by looking at the meaning of the word “exception”, we’ll see that a business exception as defined above isn’t actually an exception.

Let’s look at some definitions of the word “exception”:

A person or thing that is excluded from a general statement or does not follow a rule (Oxford Dictionary).

An instance or case not conforming to the general rule (dictionary.com).

Someone or something that is not included in a rule, group, or list or that does not behave in the expected way (Cambridge Dictionary).

All three definitions say that an exception is something that does not follow a rule which makes it unexpected.

Coming back to our example, you could say that we have used the NotEnoughFuelException as an exception to the rule “fuel tanks must contain at least 50 units of fuel”. I say, however, that we have used the NotEnoughFuelException to define the (inverted) rule “fuel tanks must not contain less than 50 units of fuel”.

After all, we have added the exception to the signature of the takeOff() method. What is that if not defining some sort of expected outcome that’s relevant for the client code to know about?

To sum up, exceptions should be exceptions. Exceptions should not be an expected outcome. Otherwise we defy the english language.

#2: Exceptions are Expensive

What should the client code do if it encounters a NotEnoughFuelException?

Probably, it will fill the fuel tanks and try again:

class FlightControl {

  void start(){
    Rocket rocket = new Rocket();
    try {
      rocket.takeOff();
    } catch (NotEnoughFuelException e) {
      rocket.fillTanks();
      rocket.takeOff();
    }
  }
  
}

As soon as the client code reacts to an exception by executing a different branch of business code, we have misused the concept of exceptions for flow control.

Using try/catch for flow control creates code that is

  • expensive to understand (because we need more time to understand it), and
  • expensive to execute (because the JVM has to create a stacktrace for the catch block).

And, unlike in fashion, expensive is usually bad in software engineering.

Exceptions without Stacktraces?

In a comment I was made aware that Java's exception constructors allow passing in a parameter writableStackTrace that, when set to false, will cause the exception not to create a stacktrace, thus reducing the performance overhead. Use at your own peril.

#3: Exceptions Hinder Reusability

The takeOff() method, as implemented above, will always check for fuel before igniting the thrusters.

Imagine that the funding for the space program has been reduced and we can’t afford to fill the fuel tanks anymore. We have to cut corners and start the rocket with less fuel (I hope it doesn’t work that way, but at least in the software industry this seems to be common practice).

Our business rule has just changed. How do we change the code to reflect this? We want to be able to still execute the fuel check, so we don’t have to change a lot of code once the funding returns.

So, we could add a parameter to the method so that the NotEnoughFuelException is thrown conditionally:

class Rocket {

  private int fuel;

  void takeOff(boolean checkFuel) throws NotEnoughFuelException {
    if (checkFuel && this.fuel < 50) {
      throw new NotEnoughFuelException();
    }
    
    lockDoors();
    igniteThrusters();
  }
  
}

Ugly, isn’t it? And the client code still has to handle the NotEnoughFuelException even if it passes false into the takeOff() method.

Using an exception for a business rule prohibits reusability in contexts where the business rule should not be validated. And workarounds like the one above are ugly and expensive to read.

#4: Exceptions May Interfere with Transactions

If you have ever worked with Java’s or Spring’s @Transactional annotation to demarcate transaction boundaries, you will probably have thought about how exceptions affect transaction behavior.

To sum up the way Spring handles exceptions:

  • If a runtime exception bubbles out of a method that is annotated with @Transactional, the transaction is marked for rollback.
  • If a checked exception bubbles out of a method that is annotated with @Transactional, the transaction is not marked for rollback (= nothing happens).

The reasoning behind this is that a checked exception is a valid return value of the method (which makes a checked exception an expected outcome) while a runtime exception is unexpected.

Let’s assume the Rocket class has a @Transactional annotation.

Because our NotEnoughFuelException is a checked exception, our try/catch from above would work as expected, without rolling back the current transaction.

If NotEnoughFuelException was a runtime exception instead, we could still try to handle the exception like above, only to run into a TransactionRolledBackException or a similar exception as soon as the transaction commits.

Since the transaction handling code is hidden away behind a simple @Transactional annotation, we’re not really aware of the impact of our exceptions. Imagine someone refactoring a checked exception to a runtime exception. Every time this exception now occurs, the transaction will be rolled back where it wasn’t before. Dangerous, isn’t it?

#5: Exceptions Evoke Fear

Finally, using exceptions to mark failing business rules invokes fear in developers that are trying to understand the codebase, especially if they’re new to the project.

After all, each exception marks something that can go wrong, doesn’t it? There are so many exceptions to have in mind when working with the code, and we have to handle them all!

This tends to make developers very cautious (in the negative sense of the word). Where they would otherwise feel free to refactor code, they will feel restrained instead.

How would you feel looking at an unknown codebase that’s riddled with exceptions and try/catch blocks, knowing you have to work with that code for the next couple years?

What to Do Instead of Business Exceptions?

The alternative to using business exceptions is pretty simple. Just use plain code to validate your business rules instead of exceptions:

class Rocket {

  private int fuel;

  void takeOff() {
    lockDoors();
    igniteThrusters();
  }
  
  boolean hasEnoughFuelForTakeOff(){
    return this.fuel >= 50;
  }
  
}
class FlightControl {

  void startWithFuelCheck(){
    Rocket rocket = new Rocket();
    
    if(!rocket.hasEnoughFuel()){
      rocket.fillTanks();
    }
    
    rocket.takeOff();
  }
  
  void startWithoutFuelCheck(){
    Rocket rocket = new Rocket();
    rocket.takeOff();
  }
  
}

Instead of forcing each client to handle a NotEnoughFuelException, we let the client check if there is enough fuel available. With this simple change, we have achieved the following:

  • If we stumble upon an exception, it really is an exception, as the expected control flow doesn’t throw an exception at all (#1).
  • We have used normal code for normal control flow which is much better readable than try/catch blocks (#2).
  • The takeOff() method is reusable in different contexts, like taking off with less than optimal fuel (#3).
  • We have no exception that might or might not interfere with any database transactions (#4).
  • We have no exception that evokes fear in the new guy that just joined the team (#5).

You might notice that this solution moves the responsibility of checking for business rules one layer up, from the Rocket class to the FlightControl class. This might feel like we’re giving up control of our business rules, since the clients of the Rocket class now have to check for the business rules themselves.

You might notice, too, however, that the business rule itself is still in the Rocket class, within the hasEnoughFuel() method. The client only has to invoke the business rule, not know about the internals.

Yes, we have moved a responsibility away from our domain object. But we have gained a lot of flexibility, readability, and understandability on the way.

Conclusion

Using exceptions, both checked and unchecked, for marking failed business rules makes code less readable and flexible due to several reasons.

By moving the invocation of business rules out of a domain object and into a use case, we can avoid having to throw an exception in the case a business rule fails. The use case decides if the business rule should be validated or not, since there might be valid reasons not to validate a certain rule.

What are your reasons to use / not to use business exceptions?

Written By:

Tom Hombergs

Written By:

Tom Hombergs

As a professional software engineer, consultant, architect, general problem solver, I've been practicing the software craft for more than fifteen years and I'm still learning something new every day. I love sharing the things I learned, so you (and future me) can get a head start. That's why I founded reflectoring.io.

Recent Posts

Guide to JUnit 5 Functional Interfaces

In this article, we will get familiar with JUnit 5 functional interfaces. JUnit 5 significantly advanced from its predecessors. Features like functional interfaces can greatly simplify our work once we grasp their functionality.

Read more

Getting Started with Spring Security and JWT

Spring Security provides a comprehensive set of security features for Java applications, covering authentication, authorization, session management, and protection against common security threats such as CSRF (Cross-Site Request Forgery).

Read more

Creating and Publishing an NPM Package with Automated Versioning and Deployment

In this step-by-step guide, we’ll create, publish, and manage an NPM package using TypeScript for better code readability and scalability. We’ll write test cases with Jest and automate our NPM package versioning and publishing process using Changesets and GitHub Actions.

Read more