3.8. Explicitly Throwing Exceptions & Exception Propagation¶
It may seem counterintuitive but sometimes you will need to write code to instantiate and/or throw (propagate) exception objects instead of catching them. Remember, exceptions are used to indicate that something unexpected happened. They can be both informative and useful - not only to us when we use existing code but also when we write code that others will use.
Often times in this class, you will be implementing methods that others will call. If they pass in an invalid value (parameter/argument), it is better to throw an exception (and let them catch it) rather than printing a message to the screen. Throwing an exception allows them to recover from the mistake and avoids unnecessary (and sometimes confusing) output.
In this section, we will see how you can throw exceptions from code that you have written and why it may be better to throw an exception instead of handling them in certain scenarios.
Example 1: Instantiating and Throwing an Unchecked Exception
In Java, the throw keyword is used to explicitly throw an
exception. When a throw
statement is encountered within a method,
that method immediately stops its execution and throws a reference
to an exception object (often containing a message about what went
wrong) to the calling method.
Don’t confuse returning a value with throwing an exception. A method in Java can either return a value (if there is no exception) or throw an exception.
We will use the code below to demonstrate how you might throw an unchecked exception to a calling method. Take a few minutes to read through the code before reading on. Write down what you think will happen in your notes.
1public class ThrowException {
2
3 public static void main(String[] args) {
4 double[] values = new double[0]; // create an array of size 0.
5 double average = ThrowException.computeAverage(values);
6 System.out.println("The average of the values in the array is: " + average);
7 } // main
8
9 public static double computeAverage(double[] nums) {
10 if (nums.length == 0) {
11 throw new IllegalArgumentException("nums array cannot be empty");
12 } // if
13 double sum = 0;
14 for (double num : nums) {
15 sum += num;
16 } // for
17 return sum / nums.length;
18 } // computeAverage
19} // ThrowException
Since the array referenced by values
is created with a size of 0
,
the computeAverage
method will create a new IllegalArgumentException
and throw a reference to that newly created object back to main
.
Note that in this scenario, the computeAverage
method does not return a value.
Important
It is not appropriate for the computeAverage
method
to handle the exception in this scenario. Instead, it should be up to
the calling method to handle the exception since that is where the
array is created. Remember, the computeAverage
method could
be called by a method other than main
(even a method in a separate class)
and could be called under a variety of input scenarios. For example, the
values in the nums
array could be provided by the user or they could
be read from a file. In the first scenario, the calling method may want
to respond by asking the user for new (or additional values). In the second
scenario, the response to the exception may be to ask for a file that contains
more values. If we were to code the exception handling in the computeAverage
method, we could only do one of these things which would limit the reusability
of the method. From the perspective of computeAverage
, it shouldn’t matter
if the values come from the user or from a file. The job for the programmer
of computeAverage
should simply be to alert the caller that something
went wrong and they can do this by throwing an exception and letting the
caller deal with the exception in an appropriate way.
Example 2: Propagating an Unchecked Exception Created Elsewhere
In this example, we will allow an exception created in another method to propagate to the caller. Here, we don’t have to explicitly throw the exception. Implicitly, we propagate (throw) the exception simply by intentionally choosing not to catch it.
1public class PropagateException {
2
3 public static void main(String[] args) {
4 double[] values = new double[0]; // create an array of size 0.
5 double max = PropagateException.computeMax(values);
6 System.out.println("The maximum value of the array is: " + max);
7 } // main
8
9 public static double computeMax(double[] nums) {
10
11 // This line causes an ArrayIndexOutOfBoundsException
12 double max = nums[0];
13
14 for (double num : nums) {
15 if (num > max) {
16 max = num;
17 } // if
18 } // for
19 return max;
20 } // computeMax
21} // PropagateException
This example works similar to example 1 in that it will crash when
we run it. However, we did not have to instantiate the exception
object or explicitly use throw
. The exception object is still
created and it is still thrown. However, the throw
happens internally
when Java tries to access the first element of an empty array on
line 12
of the code.
The exception object is created on line 12
, and because it isn’t
caught inside of computeMax
, it automatically propagates to
the calling method main
. Since main
doesn’t catch it, the
exception propagates outside of main
and crashes the program.
Test Yourself
Take a few minutes to compare the two examples above. Why did the first example require us to instantiate an object and throw it where the second example did not?
Now, attempt to compile example 2 on Odin. Read the error message
you receive and make sure you understand it. Then, catch the
exception in main
and make sure you recover by printing an
appropriate error message to the user instead of crashing.
Example 3: Throwing/Propagating a Checked Exception
We can instantiate and throw
both checked and unchecked exceptions.
However, there is one important difference when you throw or propagate
a checked exception. In these cases, you need to add a throws
clause
to the method signature to tell the compiler that it is your intent
to propagate the exception. Remember, the compiler forces us to deal
with checked exceptions in one of two ways. If we choose not to catch
them, we must explicitly propagate them using throws
.
In the example below, we show how this can be done:
1import java.io.File;
2import java.io.FileNotFoundException;
3import java.util.Scanner;
4
5public class PropagateChecked {
6
7 public static void main(String[] args) {
8 String line = PropagateChecked.firstLine("notes.txt");
9 System.out.println("The first line is: " + line);
10 } // main
11
12 public static String firstLine(String filename) throws FileNotFoundException {
13 File file = new File(filename);
14
15 // Throws a FileNotFoundException if notes.txt doesn't exist
16 Scanner input = new Scanner(file);
17
18 String first = input.nextLine();
19 return first;
20 } // firstLine
21} // PropagateChecked
Since the firstLine
method is known to throw an exception,
we would usually want to handle the exception by placing the code in
a try block. However, in this scenario, it is better to propagate the exception
to the caller so they can handle the exception.
Notice that because FileNotFoundException
is a checked exception
we have to explicitly propagate it by adding the throws
keyword
in the signature of the method.
Using the throws
keyword, we told Java that the
FileNotFoundException
will not be handled directly in this
method. Instead, it will be propagated up to the calling method, i.e.,
the method (or methods), somewhere else, that is calling
firstLine
. In that other method, the programmer can either
handle the exception (using a try-catch) or choose to propagate it again
by repeating the throws
in the calling method’s signature.
Important
In Java, checked exceptions must either be handled directly using a
try-catch or propagated up using throws
. Note that while it is
possible to place a throws
in the signature of a program’s main
method, doing so is strongly discouraged as exceptions propagated past
main
will always cause the program to crash.
Important
Understanding the difference between throw
, throws
, and
@throws
:
The
throw
keyword is used in a block of code to explicitly throw an exception. This is desirable when you want your method to throw an exception under some predefined conditions. If the exception object being thrown usingthrow
is a checked exception, then you might also need to includethrows
in the method signature.The
throws
keyword is used in a method or constructor signature to list the checked exceptions that the method is allowed to propagate.The
@throws
tag is a Javadoc tag that is used in the Javadoc comment associated with a method (or constructor) to document that it can throw an exception under certain circumstances. The rule of thumb is this: if your method throws an exception (checked or unchecked) that a user of your method should handle, then you should document that exception using@throws
in the associated Javadoc comment.
Rapid Fire Review
Which keyword is used to explicitly throw an exception?
catch
return
throw
propagate
throws
Which keyword is used to indicate that a method might throw a checked exception and that our intent is to propagate that exception if it occurs?
catch
return
throw
propagate
throws
Why might it be better to throw an exception rather than handle it within a particular method?
To ensure the method returns a value.
To allow the calling method to handle the exception in different ways.
To prevent the method from crashing.
To make the method shorter.
To pass work to someone else because we don’t have time to do it.
What happens when a method encounters a throw statement?
The method continues execution and prints an error message.
The method stops execution and returns a default value.
The method stops execution and throws an exception to the calling method.
The method retries the operation.
The method asks the user for better input.
Why doesn’t the
computeMax
method need to explicitly throw an exception when accessing the first element of an empty array?the exception is caught inside
computeMax
.the exception is handled by the Java compiler.
the exception is implicitly thrown.
the method is protected against errors by default.
there is no exception caused by
computeMax
.