Search code examples
javaspring-bootlombok

Use of Lombok @SneakyThrows annotation


I am trying to use the Lombok library in my Spring Boot application. I came across the @SneakyThrows annotation, but I didn't really get the full use of it. Usually, if there could be an exception, it's always good to catch/throw it, and also the caller can handle and catch or throw it, which is a good practice. But using @SneakyThrows, we are bypassing it. So what is the real advantage of it?

I went over lots of links but they all only say about the behaviour and I didn't understand the real use case.


Solution

  • Author of the feature here - this answer is mostly objective, but there is a certain subjective nature to it all - at some point, any language feature is lightly dictatorial about telling people how one 'should' program. It cannot be avoided. Ordinarily, opinion isn't acceptable on SO, but given that this is about lombok, and [A] I wrote this feature, and [B] I remain a core committer on lombok, I guess my opinion is the answer asked for, and this cannot devolve into a war of opinions (except Roel, my co-committer, who by and large stands by these opinions).

    Usually, if there could be an exception, it's always good to catch/throw it

    Incorrect. It's usually good to catch/throw it, but not always.

    A quick review of what it does

    the concept of the checked exception is entirely a figment of javac's imagination. The reason this doesn't compile:

    public void foo() {
      throw new IOException();
    }
    

    is because javac has an if construct in there that says: I won't compile this. If you hack javac and remove that construct, the resulting class file is completely fine. The class verifier (the code in the JVM that checks if executing the code in a class file would cause issues) doesn't care. The JVM doesn't care either. It will dutifully run that code and just... throw the exception. "But... it's not declared!", yeah, the JVM doesn't care. It doesn't know what throws IOException means. It's a comment, as far as java.exe is concerned. This explains why languages like kotlin that are targeted to run on the JVM can do what they do (they don't have checked exceptions - you can throw whatever you like regardless of throws clause. Including java-written checked exceptions).

    @SneakyThrows is like that: It lets you throw exceptions without declaring that you do so. The exception is not suppressed, or wrapped, or ignored, or modified in any way. @SneakyThrows does just one job: Tell the compiler to stop erroring and just get on with it.

    The specific 2 cases where it isn't correct to either catch a checked exception, or add it to your throws line:

    Impossible checked exceptions

    The exception is declared, and therefore, you must write code that handles it (either catch it, or stick it in the throws clause of your method), however, by way of spec or possibly simply experience and code review you know for 100% sure that the exception cannot possibly happen.

    Here is a trivial example:

    new String(someByteArray, "UTF-8");
    

    This code makes a string by treating the provided byte array as containing UTF-8 encoded text. One of the exceptions that this method is declared to throw (and this is a checked exception) is UnsupportedEncodingException.

    However, this is impossible. The java virtual machine specification guarantees that UTF-8 is available. This code is entirely valid:

    try {
      new String(someByteArray, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw new InternalError("Your JDK is corrupt. Reinstall it. Hard-crashing now because continuing in the face of a corrupt JDK install is asking for trouble");
      // possibly log that and run System.exit(0) instead!
    }
    

    However, if we start writing that kind of code, where does it end? Should you write:

    String x = ...;
    try {
      x = x.toLowerCase();
    } catch (IllegalStateException e) {
      throw new RuntimeError("Your JDK is corrupt");
    }
    

    That seems rather silly. Hence, objectively, attempting to actually deal with the 'impossible unless JDK corrupt' situation of that UnsupportedEncodingException is equally silly, so the only subjective thing I am injecting here is that it is silly to try to deal with a corrupt JDK; you really can't, and if you try, you end up writing untestable non-sequitur try/catches everywhere.

    This kind of thing is exactly where SneakyThrows shines. Because you neither want to write the try/catch, nor do you want to burden callers by listing an exception type that cannot actually ever happen.

    At the time lombok's SneakyThrows feature was released, the new String(someByteArray, StandardCharsets.UTF_8) API didn't exist yet!__

    The fact that it exists now is a good thing: I'd say that any API that throws a checked exception in cases where the programmer knows it cannot possibly happen, is really cruddy API. I'm glad you now no longer have to deal with it.

    Regardless, plenty of libraries exist where this situation does occur.

    Underspecced entrypoints

    This is valid java:

    public static void main(String[] args) throws Exception {}
    

    main methods are allowed to do that. I strongly advise all java programmers to get into the habit of doing that. The notion of checked exceptions (as in, callers need to 'worry' about them) is great for library functions (any code written with the intent to be understood as an API and not as an implementation, and is to be used by code outside of the direct confines of the team that writes it. That doesn't necessarily imply 'by other people', but may imply: By another modular layer of the application we are writing).

    It is silly for application code, though!

    A lot of exceptions are effectively unhandleable. A library has no idea what that even means (e.g. the code of new FileInputStream has no idea if one could perhaps handle 'file not found' - maybe this is the 'file open' dialog of a GUI app and the GUI can simply tell the user they typed a non-existent file name and need to pick something else - that's handling it). Application code, however, always knows. They can, or they can't. Often, they can't.

    So, what do you do when you get an exception that you cannot handle?

    You have really only one option: Throw it onward and hope that some layer above you (a caller somewhere in the call chain) can handle it. Any other act is silly and results in bad code, bad behaviour. For example, this is ubiquitous and yet evil:

    try {
     stuff;
    catch (Thingie e) {
     e.printStackTrace();
    }
    

    This is the java version of basic's ON ERROR RESUME NEXT: When an error occurs, toss half of the info about it in the garbage, make it impossible for any other code to try to deal with it, log it to someplace few folks check, and then just keep going as if nothing is wrong, which likely means if something does go wrong, 85 stack traces appear because all code is written like this, and generally 'just keep going' when an exception has occurs just means more exceptions will occur soon.

    When we speak of entrypoints, which isn't just main, but is also, e.g. for web frameworks, the many many methods that serve as the point where the web framework begins calling your code (i.e. this is the entrypoint handler for the /users/{username} URL) are all 'entrypoints' and 'application code', and here's the key insight:

    It is non-trivial to deal with a generalized, unhandleable-by-the-business-logic error condition.

    For example, let's say you have a web framework entrypoint and the first thing it does is open the database to check the user's credentials. If the database is just flat out down, what should the SQLException that occurs result in, for the webbrowsing user? Surely 'an error message', but it's more complicated than that. What you optimally want to happen is:

    • The system gathers pertinent data which isn't 100% encoded in that exception. It also involves the request parameters for example, that should be quite useful. Perhaps not here (not useful to debug 'db is down'), but for many errors, useful.
    • The system encrypts all this with a server key into a blob of data.
    • The system serves an error page to the user (with code 500) and includes a form that results in an email (our server is down or in trouble, we can't do the normal thing and let them submit to our server and save it, after all, our server is probably not capable of responding if it's in dire straits, and it is, if the DB is down!), it includes the encrypted blob. This prevents issues where a hacker can use the stack trace and error relevant info to glean hidden info whilst still letting our dev team decrypt it and figure out what went wrong.
    • All this data should also go into a log someplace.

    That's decidedly non-trivial! You do not want to have to write all that logic in 5000 separate try/catch blocks all over your code base.

    The best place to handle such a generalized error response is in whatever code calls entrypoints.

    For main, that's already how it works: You can throw whatever you want, and the way java.exe deals with it, (this'd be the thread's default exception handler) is to print the type, message, stack trace, and causal chain, and then kill that thread.

    Many, many frameworks, however, messed up. For example, the servlet framework requires that you throw ServletException. That was silly of them. Lombok's @SneakyThrows can help you by letting you throw these exceptions. Usually (and this includes servlets), it works fine - the servlet entrypoint runners actually catch everything.

    There is a third useful but less advisable use of SneakyThrows:

    Milestones

    It's less pretty as a codebase, but there are cases where exceptions really can occur, but rarely will (from experience / the situation) and whilst you really ought to catch them and wrap them in a properly typed exception, you're working towards a milestone release (a proof of concept for example), and whilst you don't want to write throw-away code, you're going to leave non-crucial stuff (not crucial for a working first version) documented as 'will fix later'. In such cases, sneakythrows is better than all alternatives, because sneakythrows doesn't mess with the exception at all.