DEV Community

Discussion on: When to use exceptions?

Collapse
 
cjbrooks12 profile image
Casey Brooks

Java's concept of checked exceptions has always seems like an anti-pattern to me, not because using checked exceptions themselves are wrong or bad, but because of how lazy programmers (myself included!) tend to handle them.

Because of the verbosity involved in catching checked exceptions, it is incredibly common to not handle the exception as the developer intended it to be handled.

Given:

public String getSomeValue() throws SomeCheckedException {
    return "";
}

There are 3 common ways of ignoring checked exceptions, none of which are particularly good.

Pass it further up the call stack

public String getAnotherValue() throws SomeCheckedException {
    return getSomeValue();
}

The problem: This is the simplest approach, but makes it more difficult to decide what threw the original exception and how it should actually be handled. With every method that just throws the exception upward, you lower the chance that your program is able to recover from it cleanly, and you end up with a codebase full of checked exceptions that really aren't being handled very well at all.

Wrap it in an unchecked exception

public String getAnotherValue() {
    try {
        return getSomeValue();
    }
    catch(SomeCheckedException e) {
        throw new SomeUncheckedException(e);
    }
}

The problem: This has all the same problems as throwing the checked exception up the call stack, because you are still doing exactly that. But now, you do not have the contract of knowing that something has gone wrong, and you're likely to get strange and hard-to-debug runtime exceptions. But at least you don't have to write any more try-catch blocks around that method!

Log it and move on

public String getAnotherValue() {
    try {
        return getSomeValue();
    }
    catch(SomeCheckedException e) {
        e.printStackTrace();
        return null; // or ""
    }
}

The problem: In this method, you're not throwing exceptions far away from the source, and you're actually doing something about it right when the exception is thrown. That's good, right? Unfortunately, no, as it means you're still just ignoring the error, and furthermore now you're polluting your logs with stack traces that are technically irrelevant to the crash you are witnessing. In this example, your program might very likely crash from whoever is getting anotherValue when it comes back as null, and you'll spend forever trying to find why that value is null when the real issue is that someValue actually had the problem, not anotherValue.

So what should you do instead?!

The whole point of using checked exceptions is that it forces the programmer to acknowledge the fact that a given method can go wrong. Its problems come in the verbosity of handling those errors, and in particular, the fact that you might end up needing to catch multiple exceptions when all you really need to know is whether you got the value that you expected or not. We can get that same benefit of forcing the programmer to acknowledge errors, while also helping them handle it properly by requiring them to provide a default value to use in the case something goes wrong.

public String getAnotherValue(String defaultValue) {
    try {
        return getSomeValue();
    }
    catch(SomeCheckedException e) {
        return defaultValue;
    }
}

How does this help?

With checked exceptions, calling getAnotherValue() is likely to just propagate that exception further and further away. Wrapped runtime exceptions aren't usually noticed until the worst possible time, in production, affecting real users. Returning a pre-defined value is difficult to notice unless the method is well-documented or you're looking at the source (which may not be available if you're calling into library or platform code).

But making the signature of the method itself have the default value to return gives control back to the programming using the method. In many cases, the library doesn't know what kind of default value to return, but the person using that library method does, and it is very natural for them to provide that default value themselves. This way they do not need to write any custom exception-handling logic, and they can continue on from that method with the assurance that the data they are using is correct. Alternatively, the programmer might explicitly recognize that they don't have that default value to provide, at which point they are prompted to write the error-handling logic right there at the call-site, rather than pushing is off somewhere else.

Now you might argue that this isn't perfect (it's probably not), but it is the solution that works best for me, and I see a similar pattern implemented in a large number of libraries that I use. Sure, it hides the original SomeCheckedException that was thrown, but in reality that is just an implementation detail that probably doesn't need to be exposed anyway, and knowing whether I got exception A or B or Z really doesn't change the fact that now I need to get a default value or else crash.