Exceptions
Goals
- Throw an exception.
- Catch an exception.
- Re-throw an exception.
Concepts
- call stack
- catch
- checked exception
- exception
- handle
- raise
- throw
- unchecked exception
- unwind
Language
throw
try … catch(…) … finally
Javadoc
@throws
Library
java.io.IOException
java.io.UnsupportedEncodingException
java.lang.AssertionError
java.lang.Error
java.lang.Exception
java.lang.IllegalArgumentException
java.lang.IllegalStateException
java.lang.IndexOutOfBoundsException
java.lang.NullPointerException
java.lang.RuntimeException
java.lang.Throwable
java.lang.UnsupportedOperationException
Lesson
Things do not always go as planned. If a caller passes bad data to a method (e.g. the caller requests a method to divide by zero), or a method has problems accessing resources such as a file, it needs a way to report the problem back to the caller. Java provides a special facility for elegant error-handling, in line with most modern programming languages.
Return Codes
Historically there have been several approaches to error-reporting. One of the earliest was the use of special return values. If a method returns a value, there may be certain values that have special meaning.
We saw for example that the Point.calculateSlope(…)
method could return Double.POSITIVE_INFINITY
to indicate that the resulting slope would be vertical. But this is not an error condition; this is only a special condition. We expect vertical lines to exist just as horizontal lines exist; they merely need to be treated specially.
Point.calculateSlope(…)
method.…
/**
* Calculates and returns the slope of a line between this point
* and the given coordinates.
* @param x2 The other x coordinate.
* @param y2 The other y coordinate.
* @return The slope of the resulting line, or {@link Double#POSITIVE_INFINITY}
* if the resulting line would be vertical.
*/
public double calculateSlope(final int x2, final int y2)
{
final int deltaX = x2 - x;
final int deltaY = y2 - y;
if(deltaX == 0)
{
return Double.POSITIVE_INFINITY;
}
return (double)deltaY / (double)deltaX;
}
…
On the other hand there exist unexpected conditions that in a particular context should be understood as an error condition. Let's say that we want to create a method for a class called ComputerScreen
that will display a happy face ☺ on the screen at some coordinates. The method might look like this:
ComputerScreen.showHappyFace(…)
method.…
/**
* Shows a happy face on the screen at the given coordinates.
* @param x The horizontal coordinate; must be zero or greater.
* @param y The vertical coordinate; must be zero or greater.
*/
public void showHappyFace(final int x, final int y)
{
…
}
…
But what if one of the coordinates is negative? The happy face will never appear (which will make the user have a sad face ☹). This is not what the method expected to happen; how can it notify the calling method that something is not right? Long ago computer programs would use special return codes that represented error conditions. For example, the above method might be adapted to return a boolean
value indicating true
if success or false
for failure.
showHappyFace(…)
method with old-fashioned error handling./**
* Shows a happy face on the screen at the given coordinates.
* @param x The horizontal coordinate; must be zero or greater.
* @param y The vertical coordinate; must be zero or greater.
* @return <code>true</code> if successful showing a happy face.
*/
public boolean showHappyFace(final int x, final int y)
{
if(x < 0 || y < 0)
{
return false; //old-fashioned error handling
}
… //show the happy face
return true;
}
There are many problems with “special value” error-handling, including:
- It introduces artificial values; why do we need to return a value for showing a face?
- It requires special checking for each call of the method, which makes the code verbose.
- It intermingles error handling into the normal logic of the program, as shown in the following example.
showHappyFace(…)
method with old-fashioned error handling.if(!showHappyFace(x, y))
{
//TODO handle error
}
if(!printResults(results))
{
//TODO handle error
}
if(!sendEmail(to, message))
{
//TODO handle error
}
The last point above is especially important: the program should tend to its normal processing of data without being distracted with error conditions. There needs to be a way to relegate error handling to a separate part of the program, outside the normal program flow.
Exceptions
Most modern programs now have the idea of exceptions. When an error condition is encountered, the program can create and then throw an exception object. (Some programming language say raise an exception.) Error handling can then be relegated to a central location in another part of your program to catch an exception that might have been thrown.
Throwing Exceptions
Throwing an exception bypasses the normal return
mechanism of a method. Processing of the method will immediately stop, and the call stack will unwind, as programmers say. That is, program execution will jump out of the current function to the previous, and then jump out of that method to the previous, and so on. This will continue until a special error-handling section of the code is reached.
One of the most common exceptions to throw is java.lang.IllegalArgumentException
, which indicates that an erroneous or inappropriate argument was passed as a parameter.
showHappyFace(…)
method using exceptions./**
* Shows a happy face on the screen at the given coordinates.
* @param x The horizontal coordinate; must be zero or greater.
* @param y The vertical coordinate; must be zero or greater.
* @throws IllegalArgumentException if one of the arguments is negative.
*/
public void showHappyFace(final int x, final int y)
{
if(x < 0 || y < 0)
{
throw new IllegalArgumentException("Negative coordinates not supported.");
}
… //show the happy face
}
Exception Handling
Once you throw an exception, something has to catch the exception at some point. Catching and processing an exception is called handling the exception, and Java uses a special try … catch(…)
statement pair for doing this.
Here is how try … catch(…)
works:
- Somewhere up the call stack (that is, in one of the callers), put all your statements that can throw some related exception(s) inside a
try{…}
block. - Immediately beneath the
try{…}
block, put acatch(…){…}
block, indicating inside the parentheses the exceptions you want to catch. - (optional) Provide a
finally{…}
block for any code you want to always execute, whether or not an exception was thrown.
showHappyFace(…)
method exception handling.try
{
showHappyFace(x, y);
printResults(results);
sendEmail(to, message);
}
catch(final IllegalArgumentException illegalArgumentException)
{
System.out.println("One of the methods didn't like the input.");
}
finally
{
System.out.println("done"); //will always print, whether or not there was an exception
}
Checked Exceptions
Exception classes such as java.lang.IllegalArgumentException
, which derive from java.lang.RuntimeException
, are called unchecked exceptions.The special low-level error classes that derive from java.lang.Error
, such as java.lang.AssertionError
, are also unchecked exceptions. Any method can throw instances of unchecked exceptions, even if the method or its documentation never indicates that it throws exceptions. Similarly there is no requirement that you catch unchecked exceptions in your program, or otherwise deal with them.
All exceptions that do not derive from java.lang.RuntimeException
are called checked exceptions. These come with two requirements:
- You must indicate using the
throws
keyword that your method may throw that type of checked exception. - The calling method must:
- catch that type of exception, and/or
- declare that it throws that type of exception.
One common checked exception is java.io.IOException
, which indicates some sort of input/output error. Here is how the sendEmail()
method above might be implemented, as sending email requires communication over TCP/IP.
sendMail(…)
method.import java.io.IOException;
…
/**
* Sends an email message.
* @param to The recipient email address.
* @param message The message to send
* @throws IllegalArgumentException if the "to" argument is not a valid email address.
* @throws IOException If the Internet connection goes down.
*/
public void sendEmail(final String to, final String message) throws IllegalArgumentException, IOException
{
if(!Email.isAddressValid(to)) //call other method to check the email address
{
throw new IllegalArgumentException("Not a valid email address.");
}
… //try to send the email
if(…) //if the Internet connection goes down
{
throw new IOException("Lost connection.")
}
}
The calling method may catch the IOException
as well:
public void giveUserFeedback()
{
try
{
showHappyFace(x, y);
sendEmail(to, message);
}
catch(final IllegalArgumentException illegalArgumentException)
{
System.out.println("One of the methods didn't like the input.");
}
catch(final IOException ioException)
{
System.out.println("The Internet connection must have went down; please try again.");
}
}
A calling method may instead decide not to catch a checked exception type, and instead simply declare that it too can throw that exception type, allowing the exception to “bubble up” the call stack if it is ever thrown.
public void giveUserFeedback() throws IOException
{
try
{
showHappyFace(x, y);
sendEmail(to, message); //any IOException thrown here will bubble up
}
catch(final IllegalArgumentException illegalArgumentException)
{
System.out.println("One of the methods didn't like the input.");
}
}
Rethrowing Exceptions
Those two approaches—catching an exception or declaring the exception using throws
—are not mutually exclusive. Part of the code could be guarded by a try … catch(…)
statement, while other portions of the method could be unguarded and allow the exception to continue up the call stack. Even if an exception is caught, it can usually be rethrown simply by using the throw
keyword again.
public void giveUserFeedback() throws IOExeption
{
try
{
showHappyFace(x, y);
sendEmail(to, message);
}
catch(final IOException ioException)
{
//TODO log the error
throw ioException; //rethrow the error
}
}
“Check” Methods
If you find yourself using the same error-checking code over and over, you may consider creating a separate method to do the check and throw the exception if needed. The name of such a method usually starts with “check…
”, and acts as a simple wrapper around the error checking logic. If everything is OK, the check method does nothing. If there is a problem, the check method will throw the appropriate exception.
public class Email
{
public static boolean isAddressValid(final String address)
{
//TODO do some checks and return true or false
}
public static void checkArgumentAddressValid(final String address) throws IllegalArgumentException
{
if(!isAddressValid(address))
{
throw new IllegalArgumentException("Invalid email address.");
}
}
/**
* Sends an email message.
* @param to The recipient email address.
* @param message The message to send
* @throws IllegalArgumentException if the "to" argument is not a valid email address.
* @throws IOException If the Internet connection goes down.
*/
public static void sendEmail(final String to, final String message) throws IllegalArgumentException, IOException
{
checkArgumentAddressValid(to); //throws an exception if invalid; otherwise does nothing
… //try to send the email
if(…) //if the Internet connection goes down
{
throw new IOException("Lost connection.")
}
}
}
Review
Exceptions are conceptually revolutionary! They represent a huge departure from the old error-handling approach of checking the return value of each method. If you don't understand how exceptions represent a different paradigm than method return values, you should review this lesson.
- Throwing an exception skips the normal control flow and unwinds the stack, providing a completely different control path for returning from a stack of method calls.
- Exceptions allows you to consolidate error handling in a specific area of the program, at the call level(s) you choose.
Common Standard Exceptions
java.lang.NullPointerException
- A
null
argument was passed when a parameter doesn't allow it. Java throws this exception automatically when you try to access the member of anull
reference. java.lang.IllegalArgumentException
- An argument passed is inappropriate for the parameter. Technically an inappropriate
null
argument is also an “illegal argument” as well, but for null references Java convention is to use aNullPointerException
. java.lang.IndexOutOfBoundsException
- The index of an array or list it outside the range. This could technically be considered an “illegal argument” as well, but is conventionally more appropriate for invalid indexes.
java.lang.IllegalStateException
- The state of some values is unexpected or inappropriate to complete the operation requested in a method. In cases where both an argument is inappropriate because of an invalid object state, only throw
IllegalArgumentException
if there is exists some argument that could have worked with the current state; otherwise, if no argument would have worked, throw anIllegalStateException
. java.lang.UnsupportedOperationException
- An object does not support a method, such as a read-only object that does not support modification methods. This exception is useful to use temporarily if you add a necessary method but have not implemented it.
Gotchas
- Only throw an exception if the condition is truly unexpected and erroneous. “Infrequent but valid” is not the same thing as “unexpected”.
- If you need to guarantee that something happens after an operation, use a
try … finally(…)
block don't just assume that the operation will not throw an exception, even if one is not listed in thethrows
clause. (You never know when an unchecked exception could be thrown.)
In the Real World
- Try to use some of the standard exception classes rather than inventing your own. This will make your program more understandable, and help it interact with other libraries.
- It is very bad practice to “eat” exceptions, meaning to silently ignore exceptions. If you catch an exception, do something with it. If you have nothing specifically to do with the exception, rethrow it or at the very least log it.
Exception Messages
Real-world applications normally have to contend with several types of messages for different purposes. The message placed in an exception when it is created is meant more for the developer, not the user. The exception usually contains a stack trace, which is of little use to a normal user. The exception message might even contain confidential information that should not be given to the end user.
This is why the types of exceptions used are so important. Normally a program would provide some friendlier message to the user, perhaps even in the user's own language if different from the application default. The Nevertheless the exception message might be useful to place in a log, to help a developer or other technical support representative track down the problem.
Thus when you catch an exception it might be useful to do several things.
- Log the exception, its message, and its stack trace. Logging is explained in a future lesson.
- Present a friendly message to the user. Providing custom messages to the user in the appropriate language is discussed in a future lesson on internationalization.
Think About It
- Is this really an error condition? Or is it just an uncommon but perfectly valid condition? Without trying to “correct” or change the input, can I process it in some valid way without creating an error? For example, I may not have expected an empty array in a
sum()
method, but isn't it valid to create a sum of zero numbers? - Do I have an appropriate
try … catch(…)
block at some level for the unchecked exceptions of the methods I call? - Did I document the unchecked exceptions I throw, using the
@throws
Javadoc tag?
Self Evaluation
- How would you determine the next line of code executed after a
throw
statement? - Everything that can be thrown using the
throw
keyword is derived from which class? - What is the difference between checked and unchecked exceptions?
- What are the two types of unchecked exceptions? What classes are they derived from?
- Is the message contained in an exception always appropriate to display to the user?
Task
Modify the Book.getYearsInPrint()
method to make sure that the answer is valid. If it yields a negative number, that means the book's publishing date was set to some date past the CURRENT_YEAR
, so throw an exception.
In the Booker method that prints all books, catch the exception representing an invalid publishing date for a single book, and inform the user that some unusual state or invalid data was encountered. Then continue printing the rest of the books.
See Also
- Exceptions (Oracle - The Java™ Tutorials)
- Catching Multiple Exception Types and Rethrowing Exceptions with Improved Type Checking (Oracle - Java™ Platform Overview)
References
- The Java® Language Specification, Java SE 11 Edition: Chapter 11. Exceptions (Oracle)
- Explanations of common Java exceptions This is a list of actual exceptions, but the descriptions are meant to be humorous and not true explanations.