Detailed Notes On Using Exceptions In Java

Below is a summary of the main concepts to consider when using exceptions in Java as discussed in Chapter 10 of the Effective Java book (Third Edition) by Joshua Bloch.

You can find a short dot point list of these points at the end of the post.

A well-designed API must not force its clients to use exceptions for ordinary control flow.

  • Instead, do:
    • Use a separate state-testing method
    • Have a state-dependent method return an empty optional or a distinguished value such as null if it cannot perform the desired computation
  • Guidelines to choose between the above two options:
    • Use optional or distinguished return value:
      • If state might be changed between invocations
      • Performance reasons
    • A state-testing method is mildly preferable to a distinguished return value:
      • Incorrect use throws an exception
      • But forgetting to check for a distinguished return value might result in a hard to detect bug
      • Not an issue for optional return values

Checked exceptions, runtime exceptions, errors:

  • Use checked exceptions for conditions from which the caller can reasonably be expected to recover
    • By confronting the user with a checked exception, the API designer presents a mandate to recover from the condition
    • Provide methods that allow for further info on the exception to aid in recovery
  • Use runtime exceptions to indicate programming errors
    • If it’s unclear whether recovery is possible, use an unchecked exception
  • Convention: errors are reserved for use by the JVM to indicate it’s impossible to continue (for various reasons)

Avoid unnecessary use of checked exceptions

  • It places a burden on the user of the API (this burden increased in Java 8 - methods throwing checked exceptions can’t be used directly in streams)
  • Burden can be justified if BOTH conditions (below) are met:
    • The exceptional condition cannot be prevented by proper use of the API
    • And, the programmer can take some useful action once confronted with the exception
  • Easiest way to eliminate checked exception —> return an optional
    • Disadvantage: can’t return additional info detailing its inability to perform desired computation
    • If more detailed info needed —> use checked exception

Favour the use of standard exceptions

  • IllegalArgumentException
  • IllegalStateException
  • NullPointerException
  • IndexOutOfBoundsException
  • ConcurrentModificationException
  • UnsupportedOperationException
  • ArithmeticException
  • NumberFormatException

*Reuse must be based on documented semantics, not just on name *Subclass a standard exception if you want to add more details *Exceptions are serializable (Chapter 12)

Throw Exceptions appropriate to the abstraction

  • Exception translation:
    • Higher layers should catch lower-level exceptions
    • In their place, throw exceptions that can be explained in terms of higher-level abstraction
  • Exception Chaining:
    • Higher-level exception’s constructor passes the cause to a chaining-aware superclass constructor
    • This is ultimately passed to one of Throwable’s chaining-aware constructors such as Throwable(Throwable) —> which provides getCause method to retrieve the cause
  • Exception translation should not be overused:
    • Best way to deal with exceptions from lower layers is to avoid them -> by ensuring lower-level methods succeed
    • If it’s impossible to prevent exceptions from lower layers -> have the higher layer silently work around these exceptions (e.g. log the exception -> allows programmer to investigate the problem, while insulating client code and the users from it)

Document all exceptions thrown by each method

  • **@throws** tags in doc comments

Include failure-capture information in detail messages

  • toString method should return as much information as possible concerning the cause of the failure
  • Should contain:
    • Values of all parameters & fields contributed to the exception
  • Do not include passwords, encryption keys, and the like in detail messages
  • Put information in constructor

Strive for failure atomicity

  • A failed method invocation should leave the object in the state that it was in, prior to the invocation -> aka. ‘Failure-atomic’
  • Several ways to achieve this effect:
    • Design immutable objects -> failure atomicity is free
    • For methods that operate on mutable objects:
      • Check parameters for validity before performing the operation -> causes most exceptions to be thrown before object modification commences
      • Order computation so that any part that may fail takes place before any part that modifies the object
      • Perform operation on a temporary copy, replace contents of object in temporary copy once operation is complete

Don’t ignore exceptions


SUMMARY:

  • Do not use Exceptions for Control Flow (instead use state-testing method or optionals)
  • Use checked exceptions if recoverable
    • Put information in constructor
    • Throw exceptions appropriate to level of abstractions (exception translation or chaining)
    • Strive for failure atomicity
  • Don’t overuse checked exceptions:
    • Use optionals if less detailed info is required
  • Use standard exceptions where possible
  • Don’t ignore exceptions

REFERENCES:

Joshua Bloch: Effective Java, Third Edition.

Written on April 12, 2018