Understanding Java Exceptions: A Deep Dive into Checked and Unchecked Exceptions

Introduction

link to this section

Exception handling is an integral part of any programming language and Java is no exception. With the help of exception handling, Java provides a powerful mechanism to handle runtime errors so that the normal flow of the application can be maintained. In this blog post, we'll discuss one of the core aspects of Java exception handling - checked and unchecked exceptions. We'll explore their characteristics, differences, and use cases to help you develop more robust and fault-tolerant Java applications.

What are Exceptions?

link to this section

In Java, an exception is an event that disrupts the normal flow of the program. It's an object that wraps an error event that occurred within a method and is passed up to the call stack. Exceptions can be generated by the Java Runtime Environment (JRE) or can be manually generated by your code.

Checked vs Unchecked Exceptions

link to this section

Java exceptions are divided into two main categories: checked and unchecked exceptions. The primary distinction between these two categories lies in how and when they're checked.

Checked Exceptions: These exceptions are checked at compile-time. This means that if a method throws a checked exception, it must also provide a handler for this exception or declare it using the throws keyword. Checked exceptions are often used for contingencies that a well-written application should anticipate and recover from. Examples include IOException , ClassNotFoundException , NoSuchFieldException , etc.

Unchecked Exceptions: These are exceptions that are checked at runtime, not at compile-time. They're also known as Runtime Exceptions. Unchecked exceptions are usually a result of programming errors such as logic errors or improper use of an API. Examples include NullPointerException , ArrayIndexOutOfBoundsException , ClassCastException , etc.

Syntax and Examples

To give a practical understanding, let's delve into some code examples.

Checked Exceptions:

import java.io.*; 
        
public class Main { 
    public static void main(String[] args) { 
        File file = new File("test.txt"); 
        FileInputStream fileInput = null; // Checked exception handling 
        
        try { 
            fileInput = new FileInputStream(file); 
        } catch (FileNotFoundException e) { 
            e.printStackTrace(); 
        } finally { 
            try { 
                if (fileInput != null) { 
                    fileInput.close(); 
                } 
            } catch (IOException e) { 
                e.printStackTrace(); 
            } 
        } 
    } 
} 

In the above example, FileNotFoundException and IOException are checked exceptions, and Java forces us to either handle these exceptions or declare them using the throws keyword.

Unchecked Exceptions:

public class Main { 
    public static void main(String[] args) { 
        String str = null; 
        
        // Unchecked exception handling 
        try { 
            System.out.println(str.length()); 
        } catch (NullPointerException e) { 
            System.out.println("NullPointerException occurred"); 
            e.printStackTrace(); 
        } 
    } 
} 

In the above example, NullPointerException is an unchecked exception. The Java compiler does not check whether an unchecked exception is handled or declared by a method.

Table of Comparision

Here is a comparative table to illustrate the differences between checked and unchecked exceptions in Java:

Checked Exceptions Unchecked Exceptions
Definition These are exceptional conditions that a well-written application should anticipate and recover from. These are exceptional conditions that are external to the application, and that the application usually cannot anticipate or recover from.
Check Time Checked at compile-time. Checked at runtime.
Subclass of Directly or indirectly inherit from the Exception class, except RuntimeException and its subclasses. Directly or indirectly inherit from the RuntimeException class.
Handling Must be declared in the method or constructor's throws clause if they can be thrown by the execution of the method or constructor and do not get caught. Do not need to be declared in a method or constructor's throws clause.
Examples IOException , ClassNotFoundException , SQLException , etc. ArithmeticException , NullPointerException , ArrayIndexOutOfBoundsException , etc.
Recoverable Typically represent recoverable conditions. Typically represent programming errors and other serious problems.
Catch or Specify Requirement Enforced by the Java compiler. Not enforced by the Java compiler.
Typical Response Applications should catch instances of this class to take corrective actions. Applications typically do not catch instances of this class or its subclasses.
Checked Exceptions Unchecked Exceptions
Use Case Mostly used for recoverable errors, such as a failure in network connectivity or a file not being available for reading. Mostly used for programming errors, such as accessing a null object or an out-of-bounds array index.
Error Source Usually caused by the environment in which an application is running. Usually result from poorly written code.
Propagation These exceptions are checked at compile-time, which means if a method is throwing a checked exception then it should handle the exception using a try-catch block or it should declare the exception, i.e., use the throws keyword. The Java compiler does not check if a method handles or declares these exceptions.
Developer Control Developer is forced to plan for the possibility of these exceptions by declaring them or handling them. It is up to the developer's discretion to catch these exceptions.
Exception Class Direct subclasses of Exception class, except RuntimeException . Subclasses of RuntimeException .
Method Signature Methods that can throw a checked exception must declare this in their signature with the throws keyword. Methods are not required to declare runtime exceptions in their signature.
Impact on Code Quality If handled improperly, can lead to cluttered code with many try-catch blocks. Can lead to more clean and readable code, as there are fewer try-catch blocks. But they require more diligent error prevention code.

Advantages of Checked and Unchecked Exceptions

link to this section

Checked Exceptions:

  • They remind the programmer to handle the exceptions.
  • Promote better coding practices by making the effects of exceptions explicit in our method APIs.

Unchecked Exceptions:

  • They avoid the excessive use of try-catch blocks which can lead to code clutter.
  • Enable developers to identify and fix programming errors during the development process.

The Throwable Superclass

link to this section

All exceptions in Java are subclasses of the Throwable class. Throwable has two direct subclasses: Error and Exception . The Error class is used for severe issues that applications typically do not catch, whereas the Exception class is meant for conditions that a reasonable application might catch. RuntimeException , a subclass of Exception , is the superclass of all the unchecked exceptions.

Unchecked Exceptions Indicate Programming Errors

link to this section

Unchecked exceptions often result from programming errors. For instance, a NullPointerException occurs when you try to access an object through a null reference, and an ArrayIndexOutOfBoundsException occurs when you try to access an array element using an illegal index. Both of these situations can be avoided with proper programming.

Checked Exceptions for Recoverable Conditions

link to this section

Checked exceptions often represent conditions from which the program might recover. For instance, if your program reads from a file and the file is not found ( FileNotFoundException ), you can prompt the user to give the correct filename. The point here is that checked exceptions encourage recovery.

Exception Handling Strategy

link to this section

A good strategy for handling exceptions is:

  • Catch unchecked exceptions only if you plan to take some corrective action, or if you want to throw another exception.
  • Propagate checked exceptions up to a method where they can be handled appropriately, or handle them where they occur.

Defining Custom Exceptions

link to this section

You can define your own exception classes to represent problems that can occur within your program. When creating your own exception class, remember to derive it from Exception directly or one of its subclasses.

If your custom exception class is intended to be thrown in response to programming errors, then unchecked exceptions are more appropriate. Conversely, if you're creating an exception that users of your program can reasonably be expected to recover from, you should create a checked exception.

// A custom checked exception 
public class CustomCheckedException extends Exception { 
    public CustomCheckedException(String message) { 
        super(message); 
    } 
} 

// A custom unchecked exception 
public class CustomUncheckedException extends RuntimeException { 
    public CustomUncheckedException(String message) { 
        super(message); 
    } 
} 

Converting Checked Exceptions to Unchecked

link to this section

Sometimes, you might find it suitable to convert a checked exception into an unchecked one. This might be because the exception is being thrown in a context where it cannot be gracefully handled, or it would be too cumbersome to declare or handle it. In such cases, you can wrap the checked exception in an unchecked exception, like RuntimeException , and rethrow it.

try { 
    // code that might throw a checked exception 
} catch (CheckedException e) { 
    throw new RuntimeException("This is a wrapper", e); 
} 

Exception Handling Best Practices

link to this section

Here are some best practices related to checked and unchecked exceptions that every Java developer should be aware of:

  • Don't Ignore Exceptions : An empty catch block defeats the purpose of exceptions. At minimum, log the exception so you can investigate the cause if the same error occurs again.
  • Throw Early, Catch Late : The best practice is to throw exceptions as early as possible in your code and catch them as late as possible. This allows you to avoid dealing with the exception in the middle of your code, which could lead to code duplication and lower maintainability.
  • Provide Useful Information : When throwing exceptions, provide as much detail as possible. This makes it easier to debug if the exception is logged or shown to an end user.
  • Don’t Throw Throwable/Error : Throwing Throwable or Error is too generic. It’s better to be specific with your exceptions so the caller can handle the exception more granely.
  • Use Standard Exceptions : Java’s built-in exceptions cover most of the generic situations. IllegalArgumentException , IllegalStateException , NullPointerException are commonly used.