DEV Community

Oliver Saywell
Oliver Saywell

Posted on

When to use exceptions?

When is the right time to use exceptions and when should you just code against something? Really interested to see what people's opinions are on this one!

Top comments (2)

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

I really like the F# guidelines for exceptions, which boils down to "use exceptions for exceptional circumstances." For example, a validation error is not really exceptional in my book. I expect them to happen. So I had rather represent them as just a normal return value instead of throwing an exception. (Plus, I can only throw one exception but I might want to return all validation errors that I can find.)

Perhaps a dependent service being down is an exceptional circumstance, perhaps not. If you are writing an internal monolithic app, it probably is exceptional in the worst way for your database to be down. But if you are doing microservices, then maybe it's not that exceptional to be unable to contact another service, so you treat it as a normal condition and implement a Circuit Breaker to handle it.

IOW, what's "exceptional" depends on the situation.

In F# specifically, most of the libraries in the ecosystem are written in C# and use exceptions for any and every kind of error. For cases which are not actually exceptional for that program, I have to catch them and return a normal value instead.

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.