Exceptions in Java

Everyday life is full of situations that we don't anticipate. For example, you get up for work in the morning and look for your phone charger, but you can't find it anywhere. You go to the bathroom to shower only to discover that the pipes are frozen. You get in your car, but it won't start. A human is able to cope with such unforeseen circumstances quite easily. In this article, we'll try to figure out how Java programs deal with them.

What's an exception?

In the programming world, errors and unforeseen situations in the execution of a program are called exceptions. In a program, exceptions can occur due to invalid user actions, insufficient disk space, or loss of the network connection with the server.
Exceptions can also result from programming errors or incorrect use of an API. Unlike humans in the real world, a program must know exactly how to handle these situations. For this, Java has a mechanism known as exception handling.

A few words about keywords

Exception handling in Java is based on the use of the following keywords in the program:

try - defines a block of code where an exception can occur;

catch - defines a block of code where exceptions are handled;

finally - defines an optional block of code that, if present, is executed regardless of the results of the try block.

These keywords are used to create special constructs in the code: try{}catch, try{}catch{}finally, try{}finally{}.

throw - used to raise an exception;

throws - used in the method signature to warn that the method may throw an exception.

An example of using keywords in a Java program:

// This method reads a string from the keyboardpublic String input()throws MyException {// Use throws to warn// that the method may throw a MyException
BufferedReader reader =newBufferedReader(newInputStreamReader(System.in));
String s = null;// We use a try block to wrap code that might create an exception. In this case,// the compiler tells us that the readLine() method in the// BufferedReader class might throw an I/O exceptiontry{
s = reader.readLine();// We use a catch block to wrap the code that handles an IOException}catch(IOException e){
System.out.println(e.getMessage());// We close the read stream in the finally block}finally{// An exception might occur when we close the stream if, for example, the stream was not open, so we wrap the code in a try blocktry{
reader.close();// Handle exceptions when closing the read stream}catch(IOException e){
System.out.println(e.getMessage());}}if(s.equals("")){// We've decided that an empty string will prevent our program from working properly. For example, we use the result of this method to call the substring(1, 2) method. Accordingly, we have to interrupt the program by using throw to generate our own MyException exception type.thrownewMyException("The string cannot be empty!");}return s;}

Why do we need exceptions?

Let's look at an example from the real world. Imagine that a section of a highway has a small bridge with limited weight capacity. If a car heavier than the bridge's limit drives over it, it could collapse. The situation for the driver would become, to put it mildly, exceptional.
To avoid this, the transportation department installs warning signs on the road before anything goes wrong.
Seeing the warning sign, a driver compares his or her vehicle's weight with the maximum weight for the bridge. If the vehicle is too heavy, the driver takes a bypass route.
The transportation department, first, made it possible for truck drivers to change their route if necessary, second, warned drivers about the dangers on the main road, and third, warned drivers that the bridge must not be used under certain conditions.
The ability to prevent and resolve exceptional situations in a program, allowing it to continue running, is one reason for using exceptions in Java. The exception mechanism also lets you protect your code (API) from improper use by validating (checking) any inputs.
Now imagine that you're the transportation department for a second. First, you need to know the places where motorists can expect trouble. Second, you need to create and install warning signs. And finally, you need to provide detours if problems arise on the main route.
In Java, the exception mechanism works in a similar way. During development, we use a try block to build an "exception barriers" around dangerous sections of code, we provide "backup routes" using a catch {} block, and we write code that should run no matter what in a finally{} block.
If we can't provide a "backup route" or we want to give the user the right to choose, we must at least warn him or her of the danger. Why? Just imagine the indignation of a driver who, without seeing a single warning sign, reaches a small bridge that he cannot cross!
In programming, when writing our classes and methods, we can't always foresee how they might be used by other developers. As a result, we can't foresee the 100% correct way to resolve an exceptional situation. That said, it's good form to warn others about the possibility of exceptional situations.
Java's exception mechanism lets us do this with the throws keyword — essentially a declaration that our method's general behavior includes throwing an exception. Thus, anyone using the method knows he or she should write code to handle exceptions.

Warning others about "trouble"

If you don't plan on handling exceptions in your method, but want to warn others that exceptions may occur, use the throws keyword.
This keyword in the method signature means that, under certain conditions, the method may throw an exception.
This warning is part of the method interface and lets its users implement their own exception handling logic. After throws, we specify the types of exceptions thrown. These usually descend from Java's Exception class. Since Java is an object-oriented language, all exceptions are objects in Java.

Exception hierarchy

When an error occurs while a program is running, the JVM creates an object of the appropriate type from the Java exception hierarchy — a set of possible exceptions that descend from a common ancestor — the Throwable class.
We can divide exceptional runtime situations into two groups:

Situations from which the program cannot recover and continue normal operation.

Situations where recovery is possible.

The first group includes situations involving an exception that descends from the Error class. These are errors that occur during due to a JVM malfunction, memory overflow, or system failure. They usually indicate serious problems that cannot be fixed by software. In Java, the possibility of such exceptions is not checked by the compiler, so they are known as unchecked exceptions.
This group also includes RuntimeExceptions, which are exceptions that descend from the Exception class and are generated by the JVM at run time. They are often caused by programming errors. These exceptions are also not checked (unchecked) at compile time, so you are not required to write code to handle them.
The second group includes exceptional situations that can be foreseen when you're writing the program (and thus you should write code to handle them). Such exceptions are called checked exceptions. When it comes to exceptions, most of a Java developer's work is handling such situations.

Creating an exception

When a program runs, exceptions are generated either by the JVM or manually using a throw statement. When this happens, an exception object is created in memory, the program's main flow is interrupted, and the JVM's exception handler tries to handle the exception.

Exception handling

In Java, we create code blocks where we anticipate the need for exception handling using the try{}catch, try{}catch{}finally, and try{}finally{} constructs.
When an exception is thrown in a try block, the JVM looks for an appropriate exception handler in the next catch block. If a catch block has the required exception handler, control passes to it. If not, then the JVM looks further down the chain of catch blocks until the appropriate handler is found.
After executing a catch block, control is transferred to the optional finally block.
If a suitable catch block is not found, then JVM stops the program and displays the stack trace (the current stack of method calls), after first performing the finally block if it exists.
Example of exception handling:

Inside print method: first step
Inside finally block
The program is running...-----------------
Exception: s is null!
Exception handled. The program will continue
Inside finally block
The program is running...-----------------
Inside print method: second step
Inside finally block
The program is running...-----------------

The finally is typically used to close any streams and free any resources opened/allocated in a try block. However, when writing a program, it's not always possible to keep track of the closure of all resources. To make our lives easier, Java's developers offer the try-with-resources construct, which automatically closes any resources opened in a try block.
Our first example can be rewritten with try-with-resources:

The bottom line

Using exceptions in Java lets you make your programs more robust by creating "backup routes", use catch blocks to separate the main code from the exception handling code, and use throws to shift the responsibility for exception handling to whoever uses your method.