Java Hack “Sneaky Throws” Explained
Every Java developer knows the fact that when a method throws a checked exception the caller of that method needs to either catch it using try-catch block or re-throw it by declaring it in the method signature.
On one hand, this is an advantage of statically typed languages like Java which can force the client to handle these exceptions. But a regular Java developer knows that in the most common cases, you don't have any logic to handle and need to terminate the flow just by throwing the error. If we deal with checked exceptions we have to chain these throws exceptions in calling methods or convert it to an unchecked exception and throw it which is not forced by the compiler. The command approach is to use unchecked exceptions and wrap the checked exception. See the below example.
public static void main(String[] args) {
try {
Files.readAllLines(Path.of("test.txt"))
.forEach(System.out::println);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
There is another hacky way to achieve the same thing without throwing RuntimeExceptions or polluting method signatures which is called sneaky throws.
If you used Project Lombok there is a high chance that you have used the annotation @SneakyThrows which magically removes the requirement of handling checked exceptions
@SneakyThrows
public static void main(String[] args) {
Files.readAllLines(Path.of("test.txt"))
.forEach(System.out::println);
}
If you think that Lambok does something like convert it to a runtime exception you are wrong. If you run this code without providing a test.txt file, the program will exit with NoSuchFileException which is an IOException which is a checked exception. So there is a way to trick the Java compiler.
Before seeing how this hack works it is important to understand that the checked exceptions are only validated at the compile time. If you see the compiled bytecode you will see there is no difference between handling checked and unchecked exception.
It uses athrow bytecode instruction. An exception table is managed at the bytecode level which has the details of the starting point and end point of the try block which is the the range that exception handled. The target line that should execute when the exception is caught and the catching exception type.
Let’s understand this with sample byte code for the first example I showed with a try-catch block. (output is generated using javap -v Main.class command). You don’t need to understand bytecode but see the exception table it has the line ranges of the try block and target is the catch block and the catching exception is IOException.
So now what happens is when an exception is thrown it checks for a matching record and goes to the target line directly if found. If not found it will pop the current stack frame and pass it to the caller of that function. This will continue until it is handled or the application with this exception.
Now we know only we need to trick the compiler because there is no difference in exception handling for both checked and unchecked exceptions at the runtime.
So for Lambok, it is easy to change the bytecode. If you see the class file that is modified by Lombok, you will see something like this.
public static void main(String[] args) {
try {
List var10000 = Files.readAllLines(Path.of("test.txt"));
PrintStream var10001 = System.out;
Objects.requireNonNull(var10001);
var10000.forEach(var10001::println);
} catch (Throwable var2) {
throw var2;
}
}
But we can't write this code and compile using Java because it requires catching the IOException. So how can we do it in plain Java? The goal is to return the same checked exception but without polluting the method signature. Here is the solution it is very simple.
public class Main {
public static void main(String[] args) {
try {
Files.readAllLines(Path.of("test.txt"))
.forEach(System.out::println);
} catch (IOException e) {
sneakyThrows(e);
}
}
public static <E extends Throwable> void sneakyThrows(Throwable e) throws E {
throw (E) e;
}
}
The easiest place to trick the compiler is when the type is inferred. In Java 8 this new type inference rule introduced that throws T (T is a generic parameter) is inferred as RuntimeException if possible. Check the official documentation if you are interested in learning more about type inference rules.
So now you can call the sneakThrows method and keep your method signatures clean. Please don’t overuse this trick, especially when writing public APIs. This will be helpful when you actually need to exit the flow when such an error occurs. Project Lambok can even make your code cleaner to achieve this with less number of lines in the code.