The Java programming language, like many others, has built-in tools for working with errors, i.e., exceptional situations (exceptions) where a program failure is handled by special code, separate from the basic algorithm.
Thanks to exceptions, a developer can anticipate weak points in the codebase and preempt fatal errors at runtime.
Therefore, handling exceptions in Java is a good practice that improves the overall reliability of code.
The purpose of this article is to explore the principles of catching and handling exceptions, as well as to review the corresponding syntactic structures in the language intended for this.
All the examples in this guide were run on Ubuntu 22.04, installed on a cloud server from Hostman.
The examples shown in this guide were run using OpenJDK. Installing it is straightforward.
First, update the list of available repositories:
sudo apt update
Next, request the list of OpenJDK versions available for download:
sudo apt search openjdk | grep -E 'openjdk-.*-jdk/'
You’ll see a short list in the terminal:
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
openjdk-11-jdk/jammy-updates,jammy-security 11.0.25+9-1ubuntu1~22.04 amd64
openjdk-17-jdk/jammy-updates,jammy-security 17.0.13+11-2ubuntu1~22.04 amd64
openjdk-18-jdk/jammy-updates,jammy-security 18.0.2+9-2~22.04 amd64
openjdk-19-jdk/jammy-updates,jammy-security 19.0.2+7-0ubuntu3~22.04 amd64
openjdk-21-jdk/jammy-updates,jammy-security,now 21.0.5+11-1ubuntu1~22.04 amd64 [installed]
openjdk-8-jdk/jammy-updates,jammy-security 8u432-ga~us1-0ubuntu2~22.04 amd64
We will use the openjdk-21-jdk
version:
sudo apt install openjdk-21-jdk
You can then check that Java was installed correctly by requesting its version:
java --version
The terminal output will look something like this:
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu122.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu122.04, mixed mode, sharing)
As shown, the exact version of OpenJDK is 21.0.5.
All the examples in this guide should be saved in a separate file with a .java
extension:
nano App.java
Then, fill the created file with an example code, such as:
class App {
public static void main(String[] args) {
System.out.println("This text is printed to the console");
}
}
Note that the class name must match the file name.
Next, compile the file:
javac App.java
And run it:
java App
The terminal will display the following:
This text is printed to the console
All exceptions in Java have a specific type associated with the reason the exception occurred—the particular kind of program failure.
There are two fundamental types of exceptions:
The Error type is only conditionally considered an exception — it's a full-fledged error that inevitably causes the program to crash.
Exceptions that can be handled using custom code and allow the program to continue executing are Checked Exceptions and Unchecked Exceptions.
Thus, errors and exceptions in Java are different entities. However, both Errors and Exceptions (Checked and Unchecked) are types with additional subtypes that clarify the reason for the failure.
Here's an example of code that triggers a compile-time exception:
import java.io.File;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
File someFile = new File("someFile.txt"); // create file reference
Scanner scanner = new Scanner(someFile); // parse file contents
}
}
Compilation will be interrupted, and you’ll see the following error in the terminal:
App.java:7: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
Scanner scanner = new Scanner(someFile);
^
1 error
If you catch and handle this exception, the code will compile and be runnable.
Here’s another example of code that triggers an exception only at runtime:
class App {
public static void main(String[] args) {
int[] someArray = {1, 2, 3, 4, 5}; // create an array with 5 elements
System.out.println(someArray[10]); // attempt to access a non-existent element
}
}
No exception will occur during compilation, but after running the compiled code, you’ll see this error in the terminal:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
at app.main(app.java:4)
This means that such an exception can be handled using user-defined code, allowing the program to continue execution.
Finally, here’s an example of code that causes a runtime error:
public class App {
static int i = 0;
public static int showSomething(int x) {
i = i + 2;
return i + showSomething(i + 2);
}
public static void main(String[] args) {
App.showSomething(i); // trigger stack overflow
}
}
The compilation will succeed, but during execution, the terminal will show a StackOverflowError
:
Exception in thread "main" java.lang.StackOverflowError
at java.base/java.io.BufferedOutputStream.implWrite(BufferedOutputStream.java:220)
at java.base/java.io.BufferedOutputStream.write(BufferedOutputStream.java:200)
at java.base/java.io.PrintStream.implWrite(PrintStream.java:643)
In this case, the error cannot be handled; it can only be fixed in the code.
Internally, all exceptions (and errors) in Java are represented as a set of classes, some of which inherit from others.
The base class for all errors and exceptions is Throwable. Two other classes inherit from it—Error and Exception, which serve as base classes for a broad range of subclasses associated with specific exception types.
The Error class describes error-type exceptions, as mentioned earlier, while the Exception class describes checked exceptions.
Furthermore, the RuntimeException class inherits from Exception and describes unchecked exceptions.
A simplified class hierarchy of Java exceptions can be represented as the following nested list:
Each exception class includes methods for retrieving additional information about the failure.
You can find the complete classification of Java exceptions, including those from additional packages, in a dedicated reference guide.
All exceptions are handled using special try and catch blocks, which are standard across most programming languages, including Java.
Inside the try block, you write code that may potentially contain an error capable of throwing an exception.
Inside the catch block, you write code that handles the exception that occurred in the previously defined try block.
For example, a try-catch structure might look like this:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
System.out.println("Who said you can’t divide by zero?");
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
}
}
}
The output of this code in the console will be:
Actually, you can't divide by zero...
This specific example is based on an illegal division by zero operation, which is wrapped in a try block and throws an ArithmeticException
.
Accordingly, in the catch block, this exception is handled by printing an error message to the console.
Thanks to this structure, the program can continue running even after the error during division by zero.
Unlike many other programming languages, Java includes a special finally block as part of the exception handling mechanism. It always executes—regardless of whether an exception occurred or not.
So, we can extend the previously shown structure:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
} finally {
// code that always executes
System.out.println("Who cares if you can divide by zero or not? This message will appear anyway!");
}
}
}
After running this code, the console will display:
Actually, you can't divide by zero...
Who cares if you can divide by zero or not? This message will appear anyway!
To understand the practical need for the finally block, consider the following example out of any specific context:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
}
// a function that hides the loading indicator
hideLoaderUI();
In a program using this structure, the hideLoaderUI()
function will never execute if an exception occurs.
In this case, you could try calling hideLoaderUI()
inside the exception handler if an exception occurs and also call it afterward if no exception occurred:
try {
parseJson(response.json);
} catch (JSONException someException) {
hideLoaderUI(); // duplicate
System.out.println("Looks like there’s something wrong with the JSON...");
}
hideLoaderUI(); // duplicate
However, this results in undesirable duplication of the function call. Moreover, instead of a function, it might be an entire block of code, and duplicating such code is considered bad practice.
Therefore, to guarantee the execution of hideLoaderUI()
without duplicating the call, you can use a finally
block:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
} finally {
// the loading indicator will be hidden in any case
hideLoaderUI();
}
Java allows you to manually create (throw) exceptions using the special throw operator:
public class App {
public static void main(String[] args) {
throw new Exception("Something strange seems to have happened...");
}
}
You can even create a variable for the exception ahead of time and then throw it:
public class App {
public static void main(String[] args) {
var someException = new Exception("Something strange seems to have happened...");
throw someException;
}
}
Another important keyword, throws
(note the “s” at the end), allows you to explicitly declare the types of exceptions (in the form of class names) that a method may throw.
If such a method throws an exception, it will propagate up to the calling code, which must handle it:
public class App {
public static void someMethod() throws ArithmeticException, NullPointerException, InterruptedException {
int someVariable = 5 / 0;
}
public static void main(String[] args) {
try {
App.someMethod();
} catch (Exception someException) {
System.out.println("Dividing by zero again? Do you even know what insanity is?");
}
}
}
The console output will be:
Dividing by zero again? Do you even know what insanity is?
The hierarchical structure of exceptions naturally allows for the creation of custom exception classes that inherit from the base ones.
Thanks to custom exceptions, Java enables you to implement application-specific error handling paths.
Thus, in addition to standard Java exceptions, you can add your own.
Each custom exception, like any predefined one, can be handled using standard try-catch-finally
blocks:
class MyOwnException extends Exception {
public MyOwnException(String message) {
super(message); // calls the parent class constructor
System.out.println("Warning! An exception is about to be thrown!");
}
}
public class App {
public static void main(String[] args) {
try {
throw new MyOwnException("Just an exception. No explanation. Anyone got a problem?");
} catch (MyOwnException someException) {
System.out.println(someException.getMessage());
}
}
}
The console output will be:
Warning! An exception is about to be thrown!
Just an exception. No explanation. Anyone got a problem?
This tutorial demonstrated, through examples, why exceptions are needed in Java, how they arise (including how to manually throw one), and how to handle them using the corresponding language tools.
Exceptions that can be caught and handled come in two types:
Checked Exceptions: Handled at compile time.
Unchecked Exceptions: Handled at runtime.
In addition to these, there are fatal errors that can only be resolved by rewriting code:
Errors: Cannot be handled.
There are several syntax structures (blocks) used to handle exceptions:
try
: Code that may throw an exception.
catch
: Code that handles the possible exception.
finally
: Code that executes regardless of whether an exception occurred.
As well as keywords to control the process of throwing exceptions:
throw
: Manually throws an exception.
throws
: Lists possible exceptions in a declared method.
You can find the full list of methods in the parent Exception class in the official Oracle documentation.