Proof By Example

Programming blog by Mark Feeney

sneakyThrow(): Checked Exceptions as Unchecked

I recently came across some suspicious code in Square’s Okio:

final class Util {
  /**
   * Throws {@code t}, even if the declared throws clause doesn't permit it.
   * This is a terrible – but terribly convenient – hack that makes it easy to
   * catch and rethrow exceptions after cleanup. See Java Puzzlers #43.
   */
  public static void sneakyRethrow(Throwable t) {
    Util.<Error>sneakyThrow2(t);
  }

  @SuppressWarnings("unchecked")
  private static <T extends Throwable> void sneakyThrow2(Throwable t) throws T {
    throw (T) t;
  }
}

Their comment admits it’s nuts, and points to Java Puzzlers, which I wasn’t aware of. Apparently I’m out of touch since this has been around since 2005, and the technique is used in many open source libraries (search around).

I had two questions: 1) how does this work (!?), and 2) why would you ever do this? Checked exceptions are a huge pain, but outright circumvention seems extreme.

Here’s some code illustrating what sneakyRethrow allows one to do:

import java.io.IOException;

class Test {

  @SuppressWarnings("unchecked")
  public static <T extends Throwable> void sneakyRethrow2(Throwable t) throws T {
    throw (T) t;
  }

  public static void sneakyRethrow(Throwable t) {
    Test.<Error>sneakyRethrow2(t);
  }

  // Naively I wondered why this wasn't the same as what sneakyRethrow
  // ultimately does
  public static void doesntWork(Throwable t) throws Error {
    throw (Error) t;
  }

  // note: IOException is checked, but no Throws declaration
  public static void test1() {
    System.out.println("sneakyRethrow");
    sneakyRethrow(new IOException("foo"));
  }

  // note: IOException is checked, still no Throws declaration
  public static void test2() {
    System.out.println("doesntWork");
    doesntWork(new IOException("foo"));
  }

  public static void main(String[] args) {
    try {
      test1();
    } catch (Exception e) {
      System.out.println("Caught: " + e);
    }
    try {
      test2();
    } catch (Exception e) {
      System.out.println("Caught: " + e);
    }
    System.out.println("done");
  }
}

This compiles, but the downcast throw (Error) t; line in doesntWork isn’t safe for most cases. There’s no guarantee you can cast an arbitrary Throwable to Error. Here is Java’s exception class hierarchy, pinched from here, with unchecked exceptions in red:

Java's exception hierarchy

Running the code demonstrates the problem with the naive doesntWork, and also shows sneakyRethrow doing its thing:

sneakyRethrow
Caught: java.io.IOException: foo
doesntWork
Caught: java.lang.ClassCastException: java.io.IOException cannot be cast to java.lang.Error
done

At this point I still didn’t know how this was possible. I read the relevant section of Java Puzzlers, but it didn’t really explain the full details of the hack, imo. One important thing it did make me aware of was this: checked exceptions are only part of Java, not the JVM. They’re only enforced by the compiler. In bytecode you can happily throw any exception from anywhere, without restrictions. Time to look at the bytecode (via javap -c Test):

    public static <T extends java/lang/Throwable> void sneakyRethrow2(java.lang.Throwable) throws T;
      Code:
         0: aload_0
         1: athrow

    public static void sneakyRethrow(java.lang.Throwable);
      Code:
         0: aload_0
         1: invokestatic  #2                  // Method sneakyRethrow2:(Ljava/lang/Throwable;)V
         4: return

    public static void doesntWork(java.lang.Throwable) throws java.lang.Error;
      Code:
         0: aload_0
         1: checkcast     #3                  // class java/lang/Error
         4: athrow

Looking at this bytecode it’s obvious why doesntWork causes a ClassCastException but sneakyRethrow doesn’t: there’s no checkcast instruction in the sneakyRethrow code path. How is this possible? sneakyThrow2 “clearly” casts the Throwable to T (which is Error) in this line: throw (T) t;. Then I clued in. Another compiler-only feature is being exploited here: generics. At runtime all the generic type information is “erased” – it’s not in the bytecode. So throw (T) t; effectively becomes throw t; (no cast), and the resulting bytecode shows this. Contrast this with doesntWork where I’ve had to explicitly cast the Throwable to Error. Nothing is erased and we get bytecode with the checkcast that leads to our ClassCastException.

Now, when would be a good time to use sneakyRethrow? Don’t use it! You agreed to use Java, pay the tax and do the right thing with checked exceptions. But then why has this code shown up in various respected Java projects? The one use case where sneakyRethrow seems to be accepted is when you catch Throwable (i.e. the most generic type of exception), do a bit of cleanup, then want to rethrow the Throwable as its most specific type. It’s well explained in this Android platform code. I’ll reproduce here for posterity:

// The following code must enumerate several types to rethrow:
public void close() throws IOException {
    Throwable thrown = null;
    ...
    if (thrown != null) {
        if (thrown instanceof IOException) {
            throw (IOException) thrown;
        } else if (thrown instanceof RuntimeException) {
            throw (RuntimeException) thrown;
        } else if (thrown instanceof Error) {
            throw (Error) thrown;
        } else {
            throw new AssertionError();
        }
    }
}

// With SneakyThrow, rethrowing is easier:
public void close() throws IOException {
    Throwable thrown = null;
    ...
    if (thrown != null) {
        SneakyThrow.sneakyThrow(thrown);
    }
}

So, like the comment in Okio says, using this in cleanup code is pretty convenient. Neat hack.

Summary

  • checked exceptions are only checked by the compiler, the JVM allows throwing any Throwable at any time
  • type parameters are compiler-only too, there is no trace of them in the bytecode (erasure)
  • combine these things to defeat the compiler and get the bytecode you want
  • never use the resulting code
  • ok, use it carefully in cleanup situations (e.g. after catching a Throwable)