Errors vs. Exceptions
Explore how Java distinguishes between recoverable exceptions and unrecoverable errors within the exception handling framework. Understand when to catch exceptions caused by application logic or external conditions and when to let critical errors from the JVM terminate the program. This lesson helps you build resilient applications by managing runtime failures properly and interpreting stack traces effectively.
Code that works on the happy path is only part of the job. Production systems also need to handle edge cases and failure scenarios. Files may be missing, network connections can fail, and application logic can trigger runtime errors such as invalid state transitions or unhandled exceptions.
In Java, we don’t treat all these failures the same way. We separate them into two distinct categories: problems we can recover from and critical system collapses beyond our control. Understanding this boundary is the first step toward writing resilient applications that fail gracefully instead of crashing blindly.
The nature of runtime failures
When a Java program runs, it relies on two things: the correctness of our logic and the stability of the environment (the Java Virtual Machine and the operating system). A failure can originate from either source.
In Java, runtime exceptions are represented as objects. When an error occurs, the JVM creates an exception object with details about the error: its cause, location, and context. However, not all exception objects serve the same purpose.
We categorize these problems based on recoverability:
Recoverable conditions: Situations where the application logic can fix the issue or offer an alternative solution (e.g., asking the user for a new filename if a file is missing).
Unrecoverable conditions: Situations where the JVM itself is unstable or broken (e.g., running out of memory). In these scenarios, the application should terminate.
This distinction is built directly into the language through two specific classes: Error and Exception.
Errors (the unrecoverable conditions)
An Error represents a serious problem that a reasonable application should not try to catch. These issues typically stem from the JVM or the underlying hardware, not from bugs in our high-level application logic.
When an Error occurs, the JVM is usually in a shaky or undefined state. Attempting to handle such an error is dangerous because the environment itself is failing. The correct response to an Error is almost always to let the program terminate immediately.
Common examples include:
OutOfMemoryError: The JVM has exhausted the heap memory allocated to it and cannot create any more objects.StackOverflowError: The application has exhausted the stack memory, usually due to infinite recursion.VirtualMachineError: The JVM itself has broken or run out of resources.
Let’s look at how easy it is to trigger a fatal system error with just a few lines of code.
Line 4: We enter the
mainmethod and immediately callcauseOverflow().Line 7: The
causeOverflowmethod calls itself.Lines 7–9: This cycle repeats infinitely. Each call adds a new frame to the stack.
Result: Eventually, the stack memory limit is reached. The JVM detects this unrecoverable state and throws a
StackOverflowError, terminating the program.
Exceptions (the recoverable conditions)
An Exception represents a condition that a program might reasonably want to catch. These are problems caused by our application code or external resources (such as files, databases, or user input). Unlike errors, exceptions do not imply that the JVM is broken. The virtual machine is fine; it is just our specific operation that failed.
Because the system is still stable, we can “recover” from these problems. For example, if we try to divide by zero, the math operation fails, but the memory and CPU continue to function correctly. We could theoretically catch that exception and display a message to the user: Please enter a non-zero number.
Common examples include:
ArithmeticException: Attempting an impossible math operation, like dividing an integer by zero.NullPointerException: Trying to use a reference that points to nothing (null).FileNotFoundException: Trying to open a file that does not exist.
Here is a simple example of an operation that causes a recoverable exception.
Lines 3–4: We define two integers, setting the
denominatorto0.Line 8: We attempt to divide
100by0. The CPU cannot perform this operation for integers.Result: The JVM stops execution of this line and constructs an
ArithmeticException. Since we rarely check for this manually in this specific snippet, the program crashes, but the failure is specific to the logic, not the system.
Comparing error and exception
While both Error and Exception signal that something went wrong, they serve very different roles in our application's lifecycle. We can think of an Exception as a flat tire (annoying, but fixable) and an Error as the road collapsing (unrecoverable).
Feature | Error | Exception |
Origin | Mostly JVM or environment issues. | Mostly application logic or external resources. |
Recoverability | Unrecoverable; the program should terminate. | Recoverable; the program can catch and handle it. |
Handling | Should not be caught or handled. | Should be caught and resolved where possible. |
Examples |
|
|
The impact of unhandled failures
By default, Java handles both errors and exceptions in the same way: if our code does not explicitly handle the problem (which we will learn to do in upcoming lessons), the JVM terminates the program and prints a stack trace.
A stack trace is a report that shows exactly where the failure occurred. It includes:
Name: What kind of problem is it? (e.g.,
java.lang.ArithmeticException).Message: A descriptive note (e.g.,
/ by zero).Location: The method and line number where the crash happened.
When we see a stack trace, our first job is to look at the problem's name. If it is an Error (like OutOfMemoryError), we generally look for leaks or configuration issues. If it is an Exception, we look at our logic to see where we mishandled data or inputs.
Distinguishing between these two types allows us to set boundaries. We try to handle exceptions because they are our responsibility. We generally do not try to handle errors because they are the environment's way of saying: I can no longer function.
We now understand that not all crashes are the same. Errors are signals from the JVM that the environment is failing, while exceptions are signals that our code or inputs are invalid. This distinction saves us from writing defensive code against impossible scenarios. We don’t need to check if the JVM is out of memory before every variable assignment; we only need to worry about the logic we control.