-
Notifications
You must be signed in to change notification settings - Fork 10.9k
ThrowablesExplained
TODO: rewrite with more examples
Guava's Throwables utility can frequently simplify dealing with exceptions.
Sometimes, when you catch an exception, you want to throw it back up to the next try/catch block. This is frequently the case for RuntimeException
or Error
instances, which do not require try/catch blocks, but can be caught by try/catch blocks when you don't mean them to.
Guava provides several utilities to simplify propagating exceptions. For example:
try {
someMethodThatCouldThrowAnything();
} catch (IKnowWhatToDoWithThisException e) {
handle(e);
} catch (Throwable t) {
Throwables.propagateIfInstanceOf(t, IOException.class);
Throwables.propagateIfInstanceOf(t, SQLException.class);
throw Throwables.propagate(t);
}
Each of these methods throw the exception themselves, but throwing the result -- e.g. throw Throwables.propagate(t)
-- can be useful to prove to the compiler that an exception will be thrown.
Here are quick summaries of the propagation methods provided by Guava:
Signature | Explanation |
---|---|
RuntimeException propagate(Throwable) | Propagates the throwable as-is if it is a RuntimeException or an Error , or wraps it in a RuntimeException and throws it otherwise. Guaranteed to throw. The return type is a RuntimeException so you can write throw Throwables.propagate(t) as above, and Java will realize that that line is guaranteed to throw an exception. |
void propagateIfInstanceOf(Throwable, Class) throws X | Propagates the throwable as-is, if and only if it is an instance of X . |
void propagateIfPossible(Throwable) | Throws throwable as-is only if it is a RuntimeException or an Error . |
void propagateIfPossible(Throwable, Class) throws X | Throws throwable as-is only if it is a RuntimeException , an Error , or an X . |
Typically, if you want to let an exception propagate up the stack, you don't need a catch
block at all. Since you're not going to recover from the exception, you probably shouldn't be logging it or taking other action. You may want to perform some cleanup, but usually that cleanup needs to happen regardless of whether the operation succeeded, so it ends up in a finally
block. However, a catch
block with a rethrow is sometimes useful: Maybe you have to update a failure count before propagating an exception, or maybe you want to propagate the exception only conditionally,
Catching and rethrowing an exception is straightforward when dealing with only one kind of exception. Where it gets messy is when dealing with multiple kinds of exceptions:
@Override public void run() {
try {
delegate.run();
} catch (RuntimeException e) {
failures.increment();
throw e;
} catch (Error e) {
failures.increment();
throw e;
}
}
Java 7 solves this problem with multicatch:
} catch (RuntimeException | Error e) {
failures.increment();
throw e;
}
Non-Java 7 users are stuck. They'd like to write code like the following, but the compiler won't permit them to throw a variable of type Throwable
:
} catch (Throwable t) {
failures.increment();
throw t;
}
The solution is to replace throw t
with throw Throwables.propagate(t)
. In this limited circumstance, Throwables.propagate
behaves identically to the original code. However, it's easy to write code with Throwables.propagate
that has other, hidden behavior. In particular, note that the above pattern works only with RuntimeException
and Error
. If the catch
block may catch checked exceptions, you'll also need some calls to propagateIfInstanceOf
in order to preserve behavior, as Throwables.propagate
can't directly propagate a checked exception.
Overall, this use of propagate
is so-so. It's unnecessary under Java 7. Under other versions, it saves a small amount of duplication, but so could a simple Extract Method refactoring. Additionally, use of propagate
makes it easy to accidentally wrap checked exceptions.
A few APIs, notably the Java reflection API and (as a result) JUnit, declare methods that throw Throwable
. Interacting with these APIs can be a pain, as even the most general-purpose APIs typically only declare throws Exception
. Throwables.propagate
is used by some callers who know they have a non-Exception
, non-Error
Throwable
. Here's an example of declaring a Callable
that executes a JUnit test:
public Void call() throws Exception {
try {
FooTest.super.runTest();
} catch (Throwable t) {
Throwables.propagateIfPossible(t, Exception.class);
Throwables.propagate(t);
}
return null;
}
There's no need for propagate()
here, as the second line is equivalent to throw new RuntimeException(t)
. (Digression: This example also reminds me that propagateIfPossible
is potentially confusing, since it propagates not just arguments of the given type but also Errors
and RuntimeExceptions
.)
This pattern (or similar variants like throw new RuntimeException(t)
) shows up ~30 times in Google's codebase. (Search for 'propagateIfPossible[^;]* Exception.class[)];'
.) A slight majority of them take the explicit throw new RuntimeException(t)
approach. It's possible that we would want a throwWrappingWeirdThrowable
method for Throwable
-to-Exception
conversions, but given the two-line alternative, there's probably not much need unless we were to also deprecate propagateIfPossible
.
In principle, unchecked exceptions indicate bugs, and checked exceptions indicate problems outside your control. (Here's a similar taxonomy.) In practice, even the JDK sometimes gets it wrong (or at least, for some methods, no answer is right for everyone).
As a result, callers occasionally have to translate between exception types:
try {
return Integer.parseInt(userInput);
} catch (NumberFormatException e) {
throw new InvalidInputException(e);
}
try {
return publicInterfaceMethod.invoke();
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
Sometimes, those callers use Throwables.propagate
. What are the disadvantages? The main one is that the meaning of the code is less obvious. What does throw Throwables.propagate(ioException)
do? What does throw new RuntimeException(ioException)
do? The two do the same thing, but the latter is more straightforward. The former raises questions: "What does this do? It isn't just wrapping in RuntimeException
, is it? If it were, why would they write a method wrapper?" Part of the problem here, admittedly, is that "propagate" is a vague name. (Is it a way of throwing undeclared exceptions?) Perhaps "wrapIfChecked" would have worked better. (But the method needs to throw, not just wrap -- "throwUncheckedOrUncheckedWrapper?") Even if the method were called that, though, there would be no advantage to calling it on a known checked exception. There may even be additional downsides: Perhaps there's a more suitable type than a plain RuntimeException
for you to throw -- say, IllegalArgumentException
.
We sometimes also see propagate
used when the exception only might be a checked exception. The result is slightly smaller and slightly less straightforward than the alternative:
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
throw Throwables.propagate(e);
}
However, the elephant in the room here is the general practice of converting checked exceptions to unchecked exceptions. It is unquestionably the right thing in some cases, but more frequently it's used to avoid dealing with a legitimate checked exception. This leads us to the debate over whether checked exceptions are bad idea in general. I don't wish to go into all that here. Suffice it to say that Throwables.propagate
does not exist for the purpose of encouraging Java users to ignore IOException
and the like.
This is probably the heart of the matter: Treating a NullPointerException
(which is a bug) and an RpcException
(which is the kind of thing that happens when there's nothing wrong with the program) in the same way is not something we want to promote. The former will generally abort a request and eventually lead to some kind of bug-tracker issue, while the latter will generally produce (say) a timeout message and bump a counter somewhere in your monitoring. Given that, we wouldn't convert between the two often. (Of course, one way to undo the wrapping is for frameworks to automatically unwrap a vanilla RuntimeException
when deciding how to respond. In the presence of such unwrapping, propagate
users might be able to get away with throw new RuntimeException(e)
.)
But what about when you're implementing a method that isn't allowed to throw exceptions? Sometimes you need to wrap your exception in an unchecked exception. This is fine, but again, propagate
is unnecessary for simple wrapping. In fact, manual wrapping may be preferable: If you wrap every exception (instead of just checked exceptions), then you can unwrap every exception on the other end, making for fewer special cases. Additionally, you may wish to use a custom exception type for the wrapping.
try {
return future.get();
} catch (ExecutionException e) {
throw Throwables.propagate(e.getCause());
}
There are multiple things to consider here:
- The cause may be a checked exception. See "Converting checked exceptions to unchecked exceptions" above. But what if the task is known not to throw a checked exception? (Maybe it's the result of a
Runnable
.) As discussed above, you can catch the exception and throwAssertionError
;propagate
has little more to offer. ForFuture
in particular, also consider Futures.getUnchecked. (TODO: say something about its additional InterruptedException behavior) - The cause may be a non-
Exception
, non-Error
Throwable
. (Well, it's unlikely to actually be one, but the compiler would force you to consider the possibility if you tried to rethrow it directly.) See "Converting fromthrows Throwable
tothrows Exception
" above. - The cause may be an unchecked exception or error. If so, it will be rethrown directly. Unfortunately, its stack trace will reflect the thread in which the exception was originally created, not the thread in which it is currently propagating. It's typically best to have include both threads' stack traces available in the exception chain, as in the
ExecutionException
thrown byget
. (This problem isn't really aboutpropagate
; it's about any code that rethrows an exception in a different thread.)
Guava makes it somewhat simpler to study the causal chain of an exception, providing three useful methods whose signatures are self-explanatory:
Throwable getRootCause(Throwable) |
---|
List<Throwable> getCausalChain(Throwable) |
String getStackTraceAsString(Throwable) |
- Introduction
- Basic Utilities
- Collections
- Graphs
- Caches
- Functional Idioms
- Concurrency
- Strings
- Networking
- Primitives
- Ranges
- I/O
- Hashing
- EventBus
- Math
- Reflection
- Releases
- Tips
- Glossary
- Mailing List
- Stack Overflow
- Android Overview
- Footprint of JDK/Guava data structures