1. Overview

In this tutorial, we'll look at the Lombok @SneakyThrows annotation.

2. Maven Dependency

We'll first add the Lombok maven dependency:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
</dependency>

3. Use @SneakyThrows

Java requires that we must either declare or handle a checked exception. Otherwise, the code won't compile. But @SneakyThrows lets us bypass this rule.

For instance, when we call a method that throws an IOException, we must either declare the IOException in our own method:

public void throwsCheckedAndDeclares() throws IOException {
    throw new IOException("Checked exception");
}

Or we must handle it with a try-catch block:

public void throwsCheckedAndHandles() {
    try {
        throw new IOException("Checked exception");
    } catch (IOException e) {
        System.out.println(e.getMessage());
    }
}

Note that this rule is only valid for checked exceptions. We don't need to handle or declare unchecked exceptions:

public void throwsUncheckedAndSkips() {
    throw new RuntimeException("Unchecked exception");
}

Here, we're throwing RuntimeException which is an unchecked exception.

Now that we've seen the regular usage, let's look at the usage of @SneakyThrows:

@SneakyThrows
public void sneakyThrowsCheckedAndSkips() {
    throw new IOException("Checked exception");
}

Although we throw an IOException here, we aren't declaring or handling. @SneakyThrows fakes out the compiler. In other words, Lombok doesn't wrap or replace the thrown checked exception but makes the compiler think that it is an unchecked exception.

Let's see another way to achieve the same result:

public void sneakyThrowsCheckedAndSkipsWithLombok() {
    try {
        throw new IOException("Checked exception");
    } catch (IOException e) {
        Lombok.sneakyThrow(e);
    }
}

In this code, we're catching the IOException and calling Lombok.sneakyThrow. In return, it just throws the same exception without any modification.

This is possible because JVM isn't aware of the distinction between checked and unchecked exceptions. Moreover, it doesn't enforce the handle-or-declare rule. This rule is enforced by the Java compiler. Consequently, when we deceive the Java compiler, our code just runs fine.

We generally catch a checked exception and wrap it inside a RuntimeException, and then throw it. @SneakyThrows reduces the number of this pattern, as a result, making the code more readable.

3.1. Configure Exception Types

We can also configure the exception types that we want to sneakily throw, in the value attribute of @SneakyThrows.

@SneakyThrows({IOException.class, ParseException.class})
public void filtersExceptions(String fileName) {
    if (fileName.startsWith("0")) {
        throw new ParseException("test", 1);
    } else {
        throw new IOException();
    }
}

The important point here is that if we list an exception that can't be thrown from the method body, the code won't compile.

3.2 Implications of Using @SneakyThrows

We can't catch sneakily thrown checked exceptions directly. This is because the Java compiler expects the exception type declared as thrown in one of the invoked methods.

Let's see with an example.

Previously, we defined the sneakyThrowsCheckedAndSkips method. When we call it from another method and try to catch IOException:

public void callSneakingThrowingChecked() {
    try {
        sampleService.sneakyThrowsCheckedAndSkips();
    } catch (IOException e) { // java: exception java.io.IOException is never thrown in body of corresponding try statement
        System.out.println(e);
    }
}

The compiler says that "java.io.IOException is never thrown in body of corresponding try statement". Although sneakyThrowsCheckedAndSkips throws IOException, the compiler isn't aware of it. Hence the compilation fails.

As a workaround, we can use a parent type - like Exception - in the catch block:

public void callSneakingThrowingChecked() {
    try {
        sampleService.sneakyThrowsCheckedAndSkips();
    } catch (Exception e) {
        System.out.println(e);
    }
}

After this change, the code compiles and the calling code can catch the sneakily thrown IOException.

4. Global Configuration

Lombok provides some configuration keys for the annotations to globally configure them.

4.1. lombok.sneakyThrows.flagUsage

We can set the lombok.sneakyThrows.flagUsage property to forbid the use of @SneakyThrows:

# [warning | error] (default: not set)
lombok.sneakyThrows.flagUsage = error

There is no default value for this property, and we're setting it as error. As a result, when Lombok detects usage of @SneakyThrows, it prints an error message like "Use of @SneakyThrows is flagged according to lombok configuration."

5. Summary

In this tutorial, we've investigated the usage of Lombok @SneakyThrows.

As always, the source code for all examples is available on Github.