Exception-Safe Java

A Word of Caution

Any decent answer
to an interesting question begins [with] "it depends..."
-- Kent Beck

These are my opinions
about a much-talked
but not-agreed topic!

In case of doubt, trust what you know. If you don't, ask!

What are exceptions?

Exceptions

Is a mechanism to
separate the code
where an error
occurred from the
code that handle it.

In Java there
are two types

  • Uncheked
  • Checked

Unchecked Exceptions

What are
unchecked exceptions?

  • Coding Errors
  • Not part of method's signature
  • Compiler won't complain
    if you don't catch it

Why unchecked
exceptions?

  • They are not meant
    to be handled
  • DON'T catch them!

Checked Exceptions

What are
checked exceptions?

  • Errors that are not
    easy to foresee
  • Part of a method's signature
  • Compiler forces you to
    handle it or propagate it

Why checked
exceptions?

  • Compiler will not
    let you forget them
  • Need to be
    part of signature
  • Methods are honest!
  • Java is the only language that have them

You can't forget that
an exception could be thrown, you need to do something about it!

Exceptions are great! You just need to learn how to use them.

Exceptions: the technicalities

Java is a
stack-oriented
programming language.

Java is compiled
to bytecode.

When a program runs, the JVM pushes to the stack all the instructions of main()

JVM pops one instruction, executes it, pops the next one, and so on.

A program ends when
there is no more
instructions to pop.

If one of the instructions invokes a method, then
its body is pushed
into the stack ...

... and then the process
of popping
bytecodes continues.

Instructions of a method form what's called
a Stack Frame.

When an instruction represents an exception to be thrown ...

The JVM looks in
an exception table if
the method knows
how to handle it.

If there is no match,
the current Stack Frame
is popped and another lookup is performed.

This process is continued until a match is found or
the stack is empty in which case program ends
with a stack trace.

If a match is found,
then the normal flow
of execution
(popping bytecodes)
is resumed!

Don't catch exceptions
too soon!

Types of error

Errors

  1. to be transformed into
    message to the user
  2. to be logged
  3. related to code
  4. to be ignored (!)

Message to the user

  • Error usually happens
    at a different layer
    (don't caught it too soon)
  • You need to provide good
    info in the message
  • Stack Trace is not acceptable!
  • Functionality is not completed

Logging

  • Functionality can
    be somehow completed
  • You need to provide
    good info, caught it
    soon but not too soon
  • Don't silence an error!

Related to code

  • Preconditions
  • Negligent coding, e.g. NPE

Ignored

  • Bad API
  • Put a comment if you
    are sure is not avoidable
  • Part of a retry strategy
     

What kind of exception would you use to
create a message?

Checked

Compiler won't let you forget you need to propagate it to UI level

What kind of exception would you use to log it?

Checked

Compiler won't let you forget you need to propagate to
appropriate method

What kind of exception would you use
for code errors?

Unchecked

Otherwise you might be tempted to catch it!

It's OK that an unchecked ends an application and a stack trace is printed!

The stack trace is meant for you: the programmer!

You'll find and fix that errors before code gets to production

Isn't it better to "silence" an error than to make a
user see a Stack Trace?

That's as bad as not handling it

Embrace unchecked exceptions, e.g. if you ever
put a null check ask yourself "what really means that
this value is null?"

Best Practices

Unchecked Exceptions

Fail Fast

public void foo(final Bar bar) {
   Objects.requireNonNull(bar);
   // use bar
}

Fail Fast

  • Validate all parameters
    from all public methods
  • Try to avoid "if ... then throw"
  • Use Objects.requireNonNull
  • Guava's Preconditions
  • Create your own fluent API
Configuration(String source, String destination, 
      int height, int width) {
  new Preconditions()
    .notNull(destination)
    .notNull(source)
    .isPositive(height)
    .isPositive(width)
    .throwIfErrorsArePresent();
  this.source = Paths.get(source);
  this.destination = Paths.get(destination);
  this.height = height;
  this.width = width;
}
public class Preconditions {
  private List<String> messages = new ArrayList<>();
  public Preconditions notNull(Object value) {
    if (value == null) {
      this.messages.add("Not present");
    }
    return this;
  }
  public Preconditions isPositive(int value) {
    if (value < 0) {
      this.messages.add("Not positive");
    }
    return this;
  }
  public void throwIfErrorsArePresent() {
    if (this.messages.size() > 0) {
      throw new BadConfigurationFileException(this.messages);
    }
  }
}

Checked
Exceptions

1. Fail Fast

2. Given the choice of
"Add throws declaration" or "Surround with try/catch", think it carefully!

Propagate the exception until you can handle it. Don't catch it too soon.

Think about your error handling strategy, don't let it be an afterthought!

To make a message, propagate it until the presentation layer.

To log it, propagate
it until you have
all the info needed.

3. When you need to
move data, create a
new exception with fields.

public InsufficientFunds extends Exception {
   private double amount;
   public InsufficientFunds(double amount) {
      this.amount = amount;
   }
   public double getAmount() {
      return amount;
   }
}

Think carefully avoid the constructors you need, is just another type!

void charge(CreditCard card, double amount) 
      throws InsufficientFunds {
   boolean canCharge = validate(card, amount);
   if (!canCharge) {
      throw new InsufficientFunds(amount);
   }
   // continue transaction
}
void foo() {
   try {
      // code at a higher layer
   } catch (InsufficientFunds e) {
      view.addObject("error", 
          "Couldn't charge " + e.getAmount());
   }
}

4. Never let a low-level exception propagate
to a higher layer.

public void charge() throws SQLException {
   // go to database and try to charge
}
public void foo() {
   try {
     // code at a higher layer
   } catch (SQLException e) {
     // handle it!
     // what happens if I refactor
     // the low-level code and a
     // new kind of exception is thrown?
   }
}

Transpose an exception to avoid exposing implementation details.

public void charge()
      throws PersistenceException {
   try {
      // code
   } catch (SQLException e) {
      throw PersistenceException(e);
   }
}

You can transpose unchecked into checked or viceversa if needed.

To transpose use
a constructor that
takes the original
exception as a parameter

public void foo() 
      throws MyAppException {
   try {
     // code
   } catch (IOException e) {
     throws MyAppException("Bad, I'm losing the previous stack trace");
   }
}
public void foo() 
      throws MyAppException {
   try {
     // code
   } catch (IOException e) {
     throws MyAppException("Much better!", e);
   }
}

5. Create a new Exception if you need its semantics to add specific error-handling

public void bar() {
   try {
     foo(); // throws IOException
     baz(); // throws IOException
   } catch (IOException e) {
     // how do I handle foo's 
     // and bar's error differently?
   }
}
public void foo() 
      throws MyAppException {
   try {
     // code
   } catch (IOException e) {
     throws MyAppException(e);
   }
}

Create new exception, transpose if need it.

public void bar() {
   try {
     foo();
     baz();
   } catch (IOException e) {
     // code to handle IOE
   } catch (MyAppException e) {
     // code to handle MAE
   }
}

Never create a new exception unless you need to carry data or avoid being caught where you handle other kind of error

5. Use multicatch to
avoid duplicate code

public void foo() {
   try {
     // code
   } catch (FileNotFound e) {
      log.warn(e);
   } catch (XMLParse e) {
      log.warn(e);
   }
}
public void foo() {
   try {
     // code
   } catch (FileNotFound | XMLParse e) {
      log.warn(e);
   }
}

6. Have only one
try/catch per method

public void foo() {
  try {

  } catch () {

  }

  // code

  try {

  } catch () {

  }
}
public void foo() {
  try {
    // code
    try {

    } catch () {

    }
    // code
  } catch () {

  }
}
public void foo() {
  try {
 
  } catch () {
    // code
    try {

    } catch () {

    }
    // code
  }
}

It's hard to reason about all flows
of execution of the method.
It's easy for bugs to go unnoticed.

public void foo() {
  try {
    // code 
  } catch (MyAppException e) {
    // handling of MAE
  }
}

7. Try to not have
code before the try nor
after the last catch

public Foo xyz() {
  Bar bar = getBar();
  Foo foo = null;
  try {
    foo = bar.getFoo();
  } catch (BooException e) {
    foo = new Foo();
  }
  return foo;
}

It's a broken window!

public Foo xyz() {
  try {
    Bar bar = getBar();
    Foo foo = bar.getFoo();
    return foo;
  } catch (BooException e) {
    return new Foo();
  }
}

8. If you need something to be visible within the catch, make it a parameter

public void foo() {
   Bar bar = getBar();
   Foo foo = bar.getFoo();
   doSomething(foo);
}

* * *

public void doSomething(Foo foo) {
   try {
     // use foo
   } catch (BooException e) {
      log.warn("This foo " + foo);
      // code
   }
}

9. Never catch Exception: you'll catch Unchecked as Checked exceptions!

10. Never throw Exception: If you throw it someone will need to catch it (see #9)

11. Remember to honor
the principle of a single
level of abstraction

private doProcessImage(Path filepath) 
    throws FileNotFoundException, IOException {
  Image image = ImageIO.read(filepath.toFile());
  if (image.isProportional(this.height, this.width)) {
    image = image.resize(this.height, this.width);
    image.write(this.destinationFolder);
  }
}

public processImage(Path filepath) {
  try {
    doProcessImage(filepath);
  } catch (FileNotFoundException e) {
    System.out.println("Path doesn't exists");
  } catch (IOException e) {
    System.out.println("File could not be read");
  }
}

12. When writing a
throws statement think
in terms of how the
caller will handle them

The Controversy

There are a lot of OOP programmers that hate checked exceptions.

Their complains are all in the lines of "misusing them"

So there is a lot of
advice that says: don't
use checked exceptions.

But the advice should be: use them correctly, isn't?

If you follow these
tips you'll avoid the
most common issues.

And there is the
Spring-argument!

JDBC API used to rely heavily on checked exceptions!

But Hibernate, and then Spring, in its wrappers decide to transpose them as unchecked exceptions.

So there are a lot of arguments like: if Spring don't use checked exceptions then I shouldn't use them either.

"... all exceptions thrown by Hibernate
were checked exceptions [...] it soon
became clear that this doesn’t make
sense, because all exceptions thrown
by Hibernate are fatal. In many cases,
the best a developer can do in this situation is to clean up, display an error message, and exit the application. Therefore, starting with Hibernate 3.x, all exceptions thrown by Hibernate are subtypes of the unchecked Runtime Exception, which is usually handled in a single location in an application."
-- Christian Bauer in Hibernate in Action

In that context, to transpose checked into unchecked exceptions make sense, which doesn't mean its true in all contexts!

I would choose an Error
over Unchecked Exceptions, but that's me...

So, if you use them
correctly, Checked Exceptions still have its
use in your API mainly because there is not a
better alternative

Or is there...?

FP programmers will say that Monads or a Disjointed Union is a better alternative!

What's their objection
to exceptions?

Exceptions breaks
referential transparecy.

An expression is referential transparent if it can
be changed by its value without changing the program behavior.

public int doubleIt(final int n) {
   return n * 2;
}

Since, for example, doubleIt(1) will always return 2, then you can safely change call of doubleIt(1) with 2 and your program will behave the same

And yes, exceptions
aren't compatible with referential transparency.

So what would be
the alternatives?

Try<T>

private ImageResizer(final Builder builder) throws IOException {
  FilesUtil.ensureDirectoryExists(builder.destination);
  this.destination = builder.destination;
  this.source = builder.source;
  this.width = builder.width;
  this.height = builder.height;
}

Non-monadic code

public static void run(final String path) {
  try {
    Properties properties = new Properties();
    properties.load(new FileInputStream(path));
    Configuration config = new Configuration(properties);
    ImageResizer resizer = new ImageResizer(config);
    resizer.run();
  } catch (IOException e) {
    System.err.println("Error :(");
  }
}

Non-monadic code

public Try<ImageResizer>(final Builder builder) throws IOException {
  return Try.run(() -> FilesUtil.ensureDirectoryExists(builder.destination))
            .map(v -> new ImageResizer(builder));
}

Try code

public static void run(final String path) {
  Try.of(Properties::new)
     .flatMap(p -> Try.of(properties.load(path)))
     .flatMap(prop -> Try.of(Configuration::new))
     .map(config -> new ImageResizer(config))
     .run(ImageResizer::run)
     .onFailure(ex -> System.err.println(":(");
}

Try code

What's cool about Try?

  • Code is easy to read
  • Is easy to propagate an error
  • Method are honest

What's not cool about Try?

  • Try<T> doesn't tell me what
    exceptions can be thrown
  • I could easily ignore / silence / forget about an exception

Either<L, R>

public static Either<IOException, Configuration> newInstance(final String path) {
  try {
    Properties prop = new Properties();
    prop.load(new FileInputStream(path));
    return Either.right(new Configuration(prop));
  } catch (IOException e) {
    return Either.left(e);
  }
}

Either code

public static void run(final String path) {
  Configuration.newInstance(path)
               .map(ImageResizer::new)
               .flatMap(ImageResizer::run)
               .orElseRun(ex -> System.err.println("Error :("));
}

Either code

What's cool about Either?

  • Code is easy to read
  • Is easy to propagate an error
  • Methods are more honest since
    in the return type we specify
    the exception thrown!

What's not cool
about Either?

  • You can't have two or more exceptions
    (it's called Either for a reason)
  • Even if you are saying on the
    Type about an exception, doesn't
    mean you'll handle it, you can
    ignore / silence / forget it.

So which is error-handling mechanism to rule them all?

None!

If I would rank them

  1. Checked Exceptions
  2. Try<T>
  3. Either<L, R>

The best error
handling should

  1. Have compiler support
  2. Be in line with Try<T>'s API
  3. But as with Either, all exceptions
    should be declared on the type

What's next?

I'm not a great programmer. I'm just a good programmer
with great habits.
-- Kent Beck

I'm not a great programmer. I just read [and code] a lot.
-- Yuji Kiriki

Image Resizer

  • Process all images on a folder,
    resize them a safe them elsewhere
  • Read a configuration file
  • It's not "my" project, so adjust to your liking!
  • Use exceptions, then use Try, then use Either, then do something "crazy"

Q&A

Thanks!
@gaijinco

Let's take a photo!

Exception-Safe Java

By Carlos Obregón

Exception-Safe Java

  • 2,374