Checked exception
From APIDesign
(→Drawbacks) |
|||
(27 intermediate revisions not shown.) | |||
Line 1: | Line 1: | ||
- | [[wikipedia::Checked_exception|Checked exceptions]] are [[Java]] invention and many like to argue that they are the worst invention ever. I like [[ | + | [[wikipedia::Checked_exception|Checked exceptions]] are [[Java]] invention and many like to argue that they are the worst invention ever. I like [[exception]]s and I like [[Checked exception]]s. One day I'll explain why. |
- | There is a really nice thing on [[checked exception]]s: if a method declares that it throws a [[checked exception]], the caller of the method has to handle it. This is a really nice language feature, if used at | + | == Don't Forget to Catch Me == |
+ | |||
+ | There is a really nice thing on [[checked exception]]s: if a method declares that it throws a [[checked exception]], the caller of the method has to handle it. This is a really nice language feature, if used at appropriate place. What is such appropriate place? If one reads a file one shall be ready for an input/output error - e.g. forcing people to catch {{JDK|java/io|IOException}} seems like the right thing to do. | ||
Thus in certain situations having [[checked exception]]s is beneficial. On the other hand, throwing [[checked exception]]s in cases where the recovery is unlikely - a frequently mentioned example is {{JDK|javax/xml/parsers|ParserConfigurationException}} - is just going to pollute the client code with useless '''catch''' statements. | Thus in certain situations having [[checked exception]]s is beneficial. On the other hand, throwing [[checked exception]]s in cases where the recovery is unlikely - a frequently mentioned example is {{JDK|javax/xml/parsers|ParserConfigurationException}} - is just going to pollute the client code with useless '''catch''' statements. | ||
- | |||
- | There is however one more example: Imagine an exception that needs to be caught when thrown from certain methods, but when it is thrown from other methods, it should behave as | + | == Can't Decide Upfront == |
+ | |||
+ | There is however one more example: Imagine an exception that needs to be caught when thrown from certain methods, but when it is thrown from other methods, it should behave as a {{JDK|java/lang|RuntimeException}} - e.g. propagate silently. We have seen an example of this recently in our [[Truffle]] project. The {{Truffle|com/oracle/truffle/api/interop|InteropException}} should smoothly propagate through many calls, but if invoked via the {{Truffle|com/oracle/truffle/api/interop|ForeignAccess}}'s '''send''' method, we want every caller to handle it. What are our [[API]] design options? | ||
=== Duplicate the Exceptions === | === Duplicate the Exceptions === | ||
Line 28: | Line 31: | ||
<source lang="java"> | <source lang="java"> | ||
throw new InteropRuntimeException(new UnknownIdentifier("name")); | throw new InteropRuntimeException(new UnknownIdentifier("name")); | ||
- | / | + | </source> |
+ | and this could be further simplified and hidden into static factory method: | ||
+ | <source lang="java"> | ||
throw UnknownIdentifierException.raise("name"); | throw UnknownIdentifierException.raise("name"); | ||
</source> | </source> | ||
- | However this still suffers from the duality of exceptions. In situations where needs to be sure, one needs to catch both exceptions '''InteropRuntimeException''' as well as {{Truffle|com/oracle/truffle/api/interop|InteropException}}, which is again a reason for few to claim that [[checked exception]]s are bad. | + | However this still suffers from the duality of exceptions. In situations where one needs to be sure, one needs to catch both exceptions '''InteropRuntimeException''' as well as {{Truffle|com/oracle/truffle/api/interop|InteropException}}, which is again a reason for a few to claim that [[checked exception]]s are bad. |
=== Unchecking Checked Exception === | === Unchecking Checked Exception === | ||
- | Luckily, if one knows the difference between [[source compatibility]] and [[binary compatibility]] one can realize that the [[JVM]] doesn't know anything about the difference between {{JDK|java/lang|RuntimeException}} and checked {{JDK|java/lang|Exception}} - it is all just a [[Java]] language construct. Other languages built on top of [[JVM]] may ignore it. And that is what we decided to do. | + | Luckily, if one knows the difference between [[SourceCompatibility|source compatibility]] and [[binary compatibility]] one can realize that the [[JVM]] doesn't know anything about the difference between {{JDK|java/lang|RuntimeException}} and checked {{JDK|java/lang|Exception}} - it is all just a [[Java]] language construct. Other languages built on top of [[JVM]] may ignore it. And that is what we decided to do. |
We designed a [[checked exception]] {{Truffle|com/oracle/truffle/api/interop|InteropException}} (and its subclasses) and added a '''raise''' method to throw the exception as unchecked one. The usage is simple: | We designed a [[checked exception]] {{Truffle|com/oracle/truffle/api/interop|InteropException}} (and its subclasses) and added a '''raise''' method to throw the exception as unchecked one. The usage is simple: | ||
Line 54: | Line 59: | ||
</source> | </source> | ||
- | == | + | == The [[Closure|Lamda]] Problem == |
- | Of course, throwing [[checked exception]]s in such a hidden way may yield some surprises. However, at least from | + | [[JDK]]8 is facing similar problem as well. Code that is using [[closures]]/lambdas can be greatly simplified compared to code with inner classes - unless it is dealing with exceptions. If you have an instance of {{JDK|java/lang/concurrent|Executor}} and you want to perform a file read in its '''execute''' method - you'll be forced to deal with the exception somehow. The usual thing won't compile: |
+ | |||
+ | <source lang="java"> | ||
+ | static void execRead(Executor secureSyncExecutor, Path path) throws IOException { | ||
+ | secureSyncExecutor.execute(() -> Files.readAllLines(path)); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | The exception raised by '''readAllLines''' is very hard to be propagated outside of the lamda. If the [[checked exception]] was converted to a hidden one by '''reraise''' method as shown in following example: | ||
+ | |||
+ | <source lang="java"> | ||
+ | static void execRead(Executor secureExecutor, Path path) throws IOException { | ||
+ | secureExecutor.execute(reraise(() -> Files.readAllLines(path))); | ||
+ | } | ||
+ | |||
+ | static Runnable reraise(Callable<?> runWithException) { | ||
+ | return () -> { | ||
+ | try { | ||
+ | runWithException.call(); | ||
+ | } catch (Exception ex) { | ||
+ | throw silenceException(RuntimeException.class, ex); | ||
+ | } | ||
+ | }; | ||
+ | } | ||
+ | |||
+ | @SuppressWarnings({"unchecked", "unused"}) | ||
+ | static <E extends Exception> RuntimeException silenceException(Class<E> type, Exception ex) throws E { | ||
+ | throw (E) ex; | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | then the usage of [[checked exception]]s would be much easier in modern [[Java]] code. All that is needed is to add the '''raise''' method into {{JDK|java/lang|Exception}}, so everyone can use it and have the '''reraise''' method somewhere or (better) have some support in [[Javac]] to automatically generate the conversion if an annotation like '''Reraise''' is used in the code: | ||
+ | |||
+ | <source lang="java"> | ||
+ | static void execRead(Executor secureExecutor, Path path) throws IOException { | ||
+ | @Reraise | ||
+ | secureExecutor.execute(Files.readAllLines(path)); | ||
+ | } | ||
+ | |||
+ | static @interface Reraise { | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | == Drawbacks == | ||
+ | |||
+ | Of course, throwing [[checked exception]]s in such a hidden way may yield some surprises. However, at least from [[API]] design perspective, the benefits seem to outweigh them. | ||
+ | |||
+ | === Throw before Return === | ||
+ | |||
+ | One problem is the return type of the '''raise''' method. The typical recommended usage is | ||
<source lang="java"> | <source lang="java"> | ||
Line 80: | Line 134: | ||
</source> | </source> | ||
- | === | + | but, of course, one can still add a never-to-be-reached return statement just as one would after calling {{JDK|java/lang|System}}.exit() method: |
+ | |||
+ | <source lang="java"> | ||
+ | int silentCheckedThrowWithReturn() { | ||
+ | UnknownIdentifierException.raise("name"); | ||
+ | return -1; | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | === Harder to Catch === | ||
Other problems may appear when catching the exception. Per [[Java]] specification it is not allowed to write | Other problems may appear when catching the exception. Per [[Java]] specification it is not allowed to write | ||
Line 108: | Line 171: | ||
and with this little change everything compiles without problems. Moreover this situation shouldn't be that common - we expect methods that assume handling of the [[checked exception]] to really declare it. | and with this little change everything compiles without problems. Moreover this situation shouldn't be that common - we expect methods that assume handling of the [[checked exception]] to really declare it. | ||
- | === | + | === Broken Catch === |
- | One more problem we have noticed so far is related to wrong assumption in a | + | One more problem we have noticed so far is related to wrong assumption in a wide catch - a catch that tries to catch everything. Following code may throw {{JDK|java/lang|ClassCastException}}: |
<source lang="java"> | <source lang="java"> | ||
Line 124: | Line 187: | ||
</source> | </source> | ||
- | However this program has always been broken from a [[JVM]] perspective. The caught {{JDK|java/lang|Throwable}} can really be anything - not only subclass of {{JDK|java/lang|RuntimeException}} or {{JDK|java/lang | + | However this program has always been broken from a [[JVM]] perspective. The caught {{JDK|java/lang|Throwable}} can really be anything - not only subclass of {{JDK|java/lang|RuntimeException}} or {{JDK|java/lang|Error}} - just normal [[Java]] programs don't expose the error. A fix is to rewrite the code to |
<source lang="java"> | <source lang="java"> | ||
Line 135: | Line 198: | ||
} | } | ||
</source> | </source> | ||
+ | |||
+ | == Summary == | ||
+ | |||
+ | We have demonstrated that [[checked exception]]s are beneficial. Not only when dealing with input and output - e.g. {{JDK|java/io|IOException}}, but also in many other cases. We have shown that the biggest problem of [[checked exception]]s - e.g. that one needs to '''decide upfront''' whether an exception is or isn't checked at declaration time isn't problem at all. The [[JVM]] doesn't make difference between normal and [[checked exception]]s and thus one can easily, and without significant drawbacks use [[checked exception]]s as unchecked ones with a simple '''raise''' method. We demonstrated that similar problems are now well recognized by the [[JDK]] itself and proposed few simple [[JDK]] [[API]] extensions that would provide standard solution for the '''checked vs. unchecked''' problem for once and ever. | ||
- | [[ | + | [[Category:APIDesignPatterns]] |
+ | [[Category:APIDesignPatterns:Exceptions]] |
Current revision
Checked exceptions are Java invention and many like to argue that they are the worst invention ever. I like exceptions and I like Checked exceptions. One day I'll explain why.
Contents |
Don't Forget to Catch Me
There is a really nice thing on checked exceptions: if a method declares that it throws a checked exception, the caller of the method has to handle it. This is a really nice language feature, if used at appropriate place. What is such appropriate place? If one reads a file one shall be ready for an input/output error - e.g. forcing people to catch IOException seems like the right thing to do.
Thus in certain situations having checked exceptions is beneficial. On the other hand, throwing checked exceptions in cases where the recovery is unlikely - a frequently mentioned example is ParserConfigurationException - is just going to pollute the client code with useless catch statements.
Can't Decide Upfront
There is however one more example: Imagine an exception that needs to be caught when thrown from certain methods, but when it is thrown from other methods, it should behave as a RuntimeException - e.g. propagate silently. We have seen an example of this recently in our Truffle project. The InteropException should smoothly propagate through many calls, but if invoked via the ForeignAccess's send method, we want every caller to handle it. What are our API design options?
Duplicate the Exceptions
We could have one RuntimeException subclass and one Exception subclass. Sometimes that might work, but in this case there are four subclasses of InteropException currently (and the number is expected to grow in the future), and having a duplicated set of classes is clearly annoying. Such solution would only support the argument of some that checked exceptions are the worst invention ever!
Wrap the Exceptions
We could introduce one RuntimeException subclass and let it carry the real checked InteropException exception. That would probably work on the catch-side:
try { // some interop code } catch (InteropRuntimeException ex) { throw (InteropException)ex.getCause(); }
and even on the throw side the code wouldn't be that bad, imagine throwing UnknownIdentifierException (a subclass of InteropException) exception:
throw new InteropRuntimeException(new UnknownIdentifier("name"));
and this could be further simplified and hidden into static factory method:
throw UnknownIdentifierException.raise("name");
However this still suffers from the duality of exceptions. In situations where one needs to be sure, one needs to catch both exceptions InteropRuntimeException as well as InteropException, which is again a reason for a few to claim that checked exceptions are bad.
Unchecking Checked Exception
Luckily, if one knows the difference between source compatibility and binary compatibility one can realize that the JVM doesn't know anything about the difference between RuntimeException and checked Exception - it is all just a Java language construct. Other languages built on top of JVM may ignore it. And that is what we decided to do.
We designed a checked exception InteropException (and its subclasses) and added a raise method to throw the exception as unchecked one. The usage is simple:
throw UnknownIdentifierException.raise("name");
E.g. the usage on the throwing side is exactly the same as in case of wrapping of the exceptions. There is just one difference - there is no wrapping. The checked exception UnknownIdentifierException is really being thrown and one can use
try { // the interop code } catch (UnknownIdentifierException ex) { System.out.println("Error " + ex.getUnknownIdentifier()); }
The Lamda Problem
JDK8 is facing similar problem as well. Code that is using closures/lambdas can be greatly simplified compared to code with inner classes - unless it is dealing with exceptions. If you have an instance of Executor and you want to perform a file read in its execute method - you'll be forced to deal with the exception somehow. The usual thing won't compile:
static void execRead(Executor secureSyncExecutor, Path path) throws IOException { secureSyncExecutor.execute(() -> Files.readAllLines(path)); }
The exception raised by readAllLines is very hard to be propagated outside of the lamda. If the checked exception was converted to a hidden one by reraise method as shown in following example:
static void execRead(Executor secureExecutor, Path path) throws IOException { secureExecutor.execute(reraise(() -> Files.readAllLines(path))); } static Runnable reraise(Callable<?> runWithException) { return () -> { try { runWithException.call(); } catch (Exception ex) { throw silenceException(RuntimeException.class, ex); } }; } @SuppressWarnings({"unchecked", "unused"}) static <E extends Exception> RuntimeException silenceException(Class<E> type, Exception ex) throws E { throw (E) ex; }
then the usage of checked exceptions would be much easier in modern Java code. All that is needed is to add the raise method into Exception, so everyone can use it and have the reraise method somewhere or (better) have some support in Javac to automatically generate the conversion if an annotation like Reraise is used in the code:
static void execRead(Executor secureExecutor, Path path) throws IOException { @Reraise secureExecutor.execute(Files.readAllLines(path)); } static @interface Reraise { }
Drawbacks
Of course, throwing checked exceptions in such a hidden way may yield some surprises. However, at least from API design perspective, the benefits seem to outweigh them.
Throw before Return
One problem is the return type of the raise method. The typical recommended usage is
void silentCheckedThrow() { throw UnknownIdentifierException.raise("name"); }
and that means the raise method must return a RuntimeException or Error. But there is no such object - there is just an instance of checked exception. That means the method raise mustn't return - it must throw the exception before returning. Thus one could also write:
void silentCheckedThrow() { UnknownIdentifierException.raise("name"); }
and the effect would be the same. Yes, it would be unless the method returns some value. The following only compiles with throw and as such the throw style is the recommended one:
int silentCheckedThrowWithReturn() { throw UnknownIdentifierException.raise("name"); }
but, of course, one can still add a never-to-be-reached return statement just as one would after calling System.exit() method:
int silentCheckedThrowWithReturn() { UnknownIdentifierException.raise("name"); return -1; }
Harder to Catch
Other problems may appear when catching the exception. Per Java specification it is not allowed to write
void uselessCatch() { try { Integer.valueOf("10"); } catch (IOException ex) { // never generated } }
as the compiler knows that the IOException is not generated in the body of the Integer parse method and refuses to compile such code. This isn't a problem in Truffle API, as we want people to only catch the InteropException when one of ForeignAccess sendXYZ methods is called - and all of them declare they throw the InteropException. Still, there is a way to fool the compiler when necessary:
void uselessCatch() { try { Integer.valueOf("10"); fakeThrow(); } catch (IOException ex) { // never generated } } void fakeThrow() throws IOException { }
and with this little change everything compiles without problems. Moreover this situation shouldn't be that common - we expect methods that assume handling of the checked exception to really declare it.
Broken Catch
One more problem we have noticed so far is related to wrong assumption in a wide catch - a catch that tries to catch everything. Following code may throw ClassCastException:
try { // do something } catch (Throwable t) { if (t instanceof Error) { throw (Error)t; } else { throw (RuntimeException)t; } }
However this program has always been broken from a JVM perspective. The caught Throwable can really be anything - not only subclass of RuntimeException or Error - just normal Java programs don't expose the error. A fix is to rewrite the code to
try { // do something } catch (Error e) { throw e; } catch (RuntimeException re) { throw re; }
Summary
We have demonstrated that checked exceptions are beneficial. Not only when dealing with input and output - e.g. IOException, but also in many other cases. We have shown that the biggest problem of checked exceptions - e.g. that one needs to decide upfront whether an exception is or isn't checked at declaration time isn't problem at all. The JVM doesn't make difference between normal and checked exceptions and thus one can easily, and without significant drawbacks use checked exceptions as unchecked ones with a simple raise method. We demonstrated that similar problems are now well recognized by the JDK itself and proposed few simple JDK API extensions that would provide standard solution for the checked vs. unchecked problem for once and ever.