Exceptions in Java can be divided into two main types: checked and unchecked.
Checked exceptions are problems that Java forces you to plan for in advance. You must either handle them or declare that your method might throw them. They're usually for external factors that you can anticipate.
Unchecked exceptions, on the other hand, are problems that Java doesn't force you to plan for. You can handle them if you want, but you're not required to. They're often due to programming errors.
Put another way, think of them like potential hazards when driving your car. Checked exceptions are like road construction or bad weather. You know these might happen, so you're required to prepare for them, like checking if chains are required on a certain route.
Unchecked exceptions are like running out of gas or getting a flat tire. These are things that could happen due to your own oversight. You're not legally required to constantly check your fuel gauge or tire pressure, but if these problems occur, your trip will still be disrupted.
Let's take a closer look at the differences with some examples.
Checked Exceptions in Java
In broad terms, a checked exception (also called a logical exception) in Java is something that has gone wrong in your code and is potentially recoverable. For example, if there’s a client error when calling another API, we could retry from that exception and see if the API is back up and running the second time. A checked exception is caught at compile time so if something throws a checked exception the compiler will enforce that you handle it.
Checked Exception Examples
Example One: FileNotFoundException
The code below shows the FileInputStream
method from the java.io
package with a red line underneath.
import java.io.File;
import java.io.FileInputStream;
public class CheckedException {
public void readFile() {
String fileName = "file does not exist";
File file = new File(fileName);
FileInputStream stream = new FileInputStream(file); //Unhandled checked exception
}
}
The red line is because this method throws a checked exception and the compiler is forcing us to handle it. You can do this in one of two ways:
Try Catch
You simply wrap the code which throws the exception within a try catch
block. This now allows you to process and deal with the exception. With this approach it's very easy to swallow the exception and then carry on like nothing happened. Later in the code when what the method was doing is required you may find yourself with our good friend the NullPointerException
.
We have now caught the exception and processed the error in a meaningful way by adding our code to the catch
block, the code sequence carries on crisis averted.
import java.io.File;
import java.io.FileInputStream; import java.io.FileNotFoundException;
public class CheckedException {
public void readFile() {
String fileName = "file does not exist";
File file = new File(fileName);
//Using try-catch block to handle the exception
try {
FileInputStream stream = new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
Throws
We use the keyword throws
to throw the exception up the stack to the calling method to handle. This is what FileInputStream
has just done to you. This looks and feels great - no messy exception code we are writing and we no longer need to handle this exception as someone else can deal with it. The calling method then needs to do something with it ... maybe throw again.
As with try catch
be wary of always throwing as you need to think who SHOULD be handling the error and what piece of code is best placed to handle it correctly.
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class CheckedException {
//Throwing the exception up the stack using throws keyword
public void readFile() throws FileNotFoundException {
String fileName = "file does not exist";
File file = new File(fileName);
FileInputStream stream = new FileInputStream(file);
}
}
Example Two: ParseException
Another common example of a checked exception in Java is the ParseException
, which happens when an input is attempted to be parsed that does not match the expected format. Here’s an example:
import java.text.SimpleDateFormat;
import java.util.Date;
public class ParseExceptionExample {
public static void main(String[] args) {
String dateString = "2024/01/01";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = dateFormat.parse(dateString); //Unhandled ParseException
}
}
Since the ParseException
is not handled in code, the compiler complains and will not compile the code until it is handled explicitly. As shown earlier, this can be done using a try-catch block or by throwing it up the stack using the throws
keyword.
Try Catch
Here’s an updated example handling the ParseException
using a try-catch block:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ParseExceptionExample {
public static void main(String[] args) {
String dateString = "2024/01/01";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
//Using try-catch block to handle the exception
try {
Date date = dateFormat.parse(dateString);
} catch (ParseException pe) {
pe.printStackTrace();
}
}
}
Throws
Here’s an updated example handling the ParseException
by throwing the exception up the stack using the throws
keyword:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ParseExceptionExample {
//Throwing the exception up the stack
public static void main(String[] args) throws ParseException {
String dateString = "2024/01/01";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = dateFormat.parse(dateString);
}
}
Unchecked Exceptions in Java
An unchecked exception (also known as an runtime exception) in Java is something that has gone wrong with the program and is unrecoverable. Just because this is not a compile time exception, meaning you do not need to handle it, that does not mean you don’t need to be concerned about it.
The most common runtime exception is the good old NullPointerException
which is when you are trying to access a variable or object that doesn’t exist.
Unchecked Exception Examples
Example One: NullPointerException
A NullPointerException
in Java occurs when you try to use an object reference that has not been initialized (i.e. it is null). Here’s an example:
public class NullPointerExceptionExample {
public static void main(String[] args) {
String[] strArray = new String[2];
System.out.println(strArray[0].length()); //Trying to call a method on a null object
}
}
Here, an array of string objects strArray
is declared but not initialized. Therefore, its elements are null. When a method is called on one of its elements, a NullPointerException
is thrown:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "strArray[0]" is null
at NullPointerExceptionExample.main(NullPointerExceptionExample.java:4)
This exception can be avoided by initializing the object before it is used. Additionally, a check should be performed to ensure the object is not null
:
public class NullPointerExceptionExample {
public static void main(String[] args) {
String[] strArray = {"Hello", "World"}; //Initializing strArray
if (strArray[0] != null) { //Check for null
System.out.println(strArray[0].length());
}
}
}
Example Two: IndexOutOfBoundsException
This is a very common runtime exception when dealing with arrays. This is telling you, you have tried to access an index in an array that does not exist. If an array has 10 items and you ask for item 11 you will get this exception for your efforts.
import java.util.ArrayList;
import java.util.List;
public class IndexOutOfBounds {
public static void main(String[] args) {
List<String> lst = new ArrayList<>();
lst.add("item-1");
lst.add("item-2");
lst.add("item-3");
var result = lst.get(lst.size());
}
}
The above piece of code is a common way to get an IndexOutOfBoundsException
:
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3
at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266)
at java.base/java.util.Objects.checkIndex(Objects.java:361)
at java.base/java.util.ArrayList.get(ArrayList.java:427)
at IndexOutOfBounds.main(IndexOutOfBounds.java:10)
The reason this trips people up is because the size of the array is 3 - makes sense; there are 3 items - but arrays are 0-based so the last item in the array is at index 2. To access the last item, it is always the size -1.
import java.util.ArrayList;
import java.util.List;
public class IndexOutOfBounds {
public static void main(String[] args) {
List<String> lst = new ArrayList<>();
lst.add("item-1");
lst.add("item-2");
lst.add("item-3");
var result = lst.get(lst.size()-1); //Using List.size()-1 to get last item in list
}
Checked Exceptions During Runtime
Below is an example that is very commonly used in micro service architecture. If we received a request and we cannot, say, read data from our database needed for this request, the database will throw us a checked exception, maybe an SQLException
or something similar. Because this data is important, we cannot fulfill this request without it. This means there is nothing we can actually do with this exception that can fix the problem, but if we do nothing the code will carry on its execution regardless.
We could throw the exception to the calling code until we get to the top of the chain and return the exception to the user. By doing that we are then littering all the layers above with an exception that they really do not care about, nor should they. What we really want is a RuntimeException
to terminate this request gracefully.
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class CheckedException {
public void readFile() {
String fileName = "file does not exist";
File file = new File(fileName);
try {
FileInputStream stream = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new ProcessingException("Error opening file"); }
}
}
}
Above we have our same piece of code for handling the checked exception thrown from the FileInputStream
constructor but this time we are throwing our own RuntimeException
and because this exception isn’t checked at compile time we don’t need to declare it.
public class ProcessingException extends RuntimeException {
public ProcessingException(String message) {
super(message);
}
}
Declaring your own exception type is as simple as extending the runtime exception class because as we have seen from the diagram at the top, RuntimeException
is a subtype of Exception
.
Summary
So to summarize, the difference between a checked and unchecked exception is:
- A checked exception is caught at compile time whereas a runtime or unchecked exception is, as it states, at runtime.
- A checked exception must be handled either by re-throwing or with a
try catch
block, whereas a runtime isn’t required to be handled. - A runtime exception is a programming error and is fatal whereas a checked exception is an exception condition within your code’s logic and can be recovered or re-tried from.