Friday, November 8, 2013

Java: Should I assert or throw AssertionError?

Assertions were introduced to the Java language in JDK 1.4, that was in 2002. It brought the assert statement
assert x != null : "x was null!";
and the throwable error class
if (x==null) throw new AssertionError("x was null!");
Which one should I use in my code?

Well then, what's the difference?

The assert statement

Unless assertions are actively turned on when running the program (-ea), they are not executed. They are still in the bytecode, but they don't run.
  • Bugs go unnoticed.
    These bugs should have been detected already in unit tests and the testing phase before deployment when running with assertions on. But experience taught us that some slip through.
  • No performance loss.
    There are a few rare scenarios where it matters.
  • Side effects are possible.
    Badly programmed assertions can cause nasty side effects. Because sometimes the assert code is executed, and other times it's not, the program may behave slightly different.
Using the assert statement gives control to the runtime config whether they run or not. The choice is left to the person who runs the program. The choice can be made from run to run, no recompile is needed.

The AssertionError

The asserting code is always executed. Changing your mind about execution means a recompile and re-deployment.
  • Bugs always show up.
    Even in production they abort the program.
  • Performance loss.
    In most cases it's irrelevant.
  • Guaranteed to have no side effects.
    The executed code is always the same.


Vital or nice to have?

Programs were written in Java long before assertions were available. Some came up with their own self-made assertions logic - which was not as clean and short as the real thing. And most probably didn't.
Assertions are not allowed to alter the program logic, were not always available, and are not mandatory. One can very well run the same program with them turned off. Conclusion: nice to have.

However, it is not guaranteed that a specific bug ever shows up without assertions running. Let's take a flight simulator for example. Assume a computation bug that only occurs in a rarely executed code path. The bug can cause the airplane to fly at slightly reduced speed, and no one ever notices. Or it can be a number overflow, causing the plane to fly backwards. That will surely be seen each time.

Would assertions help in production code?

It depends on the domain. Assertion means abort. Is that what you'd want, to prevent the worst? Or would you rather try to go on, hoping that it's a minor, neglegtible bug?
  1. End-of-day accounting program: rather abort, alert the technicians, they fix it, and go on. No damage done, and bug fixed.
  2. Real-time program where thousands of employees depend on it, and an abort means a big loss of work time. No abort. Hope that the bug eventually shows up as a side effect, and it can be traced and fixed.
  3. Program where an abort is the worst case scenario, like a flight simulator: go on.


assert, not AssertionError

When writing library code, assert is the right choice. It gives the power to the user whether assertions should be on or off, whether he favors detection + abort, or to go unnoticed.

When writing an application, use assert as well. If you want to enforce assertions, you can still hack in this piece of code, and if you change your mind later, you don't have to go through all assertions and replace them:
static {
    boolean assertsEnabled = false;
    assert assertsEnabled = true; // Intentional side effect
    if (!assertsEnabled)
        throw new RuntimeException("Asserts must be enabled (-ea)!");
Also, with assert, they can be enabled on a per-package level
java -ea:com.example... MyApp

AssertionError to satisfy compilation

Sometimes, when reaching code that should never be reached, an AssertionError is thrown to make the code compile. Example:
switch (TrafficLight) {
    case GREEN:
        return doGreen();
    case ORANGE:
        return doOrange();
    case RED:
        return doRed();
        //Ugh! I gotta do something, but have no clue what to do!
        //Let's abort the app and turn the semaphore offline.
        throw new AssertionError("Dead code reached");
An assert statement would not compile when a return is required.

However, in some cases, it might be more suited to throw an exception instead of an error, for example an UnsupportedOperationException. In the above case an exception barrier could catch that, then turn all semaphores at the intersection to blinking orange for a minute, and then restart the green interval as it would from a clean program start and continue normal operation. That would have several advantages:
  • be cheaper than sending a technician
  • the semaphore is off for short time only
  • while it's off it's blinking orange, rather than being totally off, that seems more secure
The problem would be detected in both cases. With an AssertionError it's in the output for free. And with UnsupportedOperationException it's the task of the one catching the exception to log it.

So again, the choice is between a hard abort, or giving the program the chance to recover and continue if a higher level decides to do so.

What I'm missing: detection and logging!

In the production scenarios 2 and 3 from above I'd want a 3rd way of handling assertions, which is not offered by Java's assertion feature.

Java has 2 strategies:
  1. Don't even check, hence no abort
  2. Check, and conditionally abort or continue
And I'm missing the 3rd one with the behavior:
  1. Run assertion code to detect bugs and don't abort, but instead log.
    It's a clear bug, it's detected, so log it. There's no cheaper way to detect it than right here, in the assert code that was written already.

Configure your IDE to run with assertions

It strikes me that the default runconfig in the most common IDEs does not have assertions enabled. It actually happened to me that I had spent way too much time chasing a bug, when the assertion would have shown it instantly - but they weren't on.

In IntelliJ IDEA: You need to modify the Defaults for all kinds that you're using: Application, JUnit, TestNG, ... and changing the defaults does not change the runconfigs you've created earlier. And you need to do this in every project, separately. (Please add a comment if you know how to set this once and for all!)

For Eclipse there's an explanation here also see the 2nd answer about JUnit tests.

No comments:

Post a Comment