Java Exception Types and Hierarchy
Java Exception Types
Understanding the different types of exceptions in Java is crucial for writing robust, maintainable applications. Java's exception system is built around a well-defined hierarchy that helps developers categorize and handle different types of errors appropriately. This systematic approach enables better error handling, debugging, and application reliability.
The Exception Hierarchy
Java's exception system is built on a clear inheritance hierarchy rooted in the Throwable
class. Understanding this hierarchy is essential for proper exception handling and creating custom exceptions.
The Complete Hierarchy:
java.lang.Object
└── java.lang.Throwable
├── java.lang.Error (Unchecked)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── VirtualMachineError
│ └── ... other system errors
└── java.lang.Exception
├── java.lang.RuntimeException (Unchecked)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ClassCastException
│ └── ... other runtime exceptions
└── Checked Exceptions
├── IOException
├── SQLException
├── ClassNotFoundException
├── InterruptedException
└── ... other checked exceptions
Key Principles:
- Throwable: Root of all exceptions and errors
- Error: Serious system-level problems that applications shouldn't try to handle
- Exception: Problems that applications can reasonably handle
- RuntimeException: Programming errors that could be prevented
- Checked Exceptions: Expected conditions that must be handled
public class ExceptionHierarchyDemo {
public static void main(String[] args) {
// Demonstrate the hierarchy
demonstrateHierarchy();
}
public static void demonstrateHierarchy() {
// All exceptions inherit from Throwable
Throwable throwable = new Exception("Base exception");
// Errors are also Throwables but indicate serious problems
// Don't create these in normal code - just for demonstration
// Error error = new OutOfMemoryError("System error");
// Runtime exceptions inherit from Exception
RuntimeException runtimeEx = new IllegalArgumentException("Runtime error");
// Checked exceptions inherit from Exception but not RuntimeException
Exception checkedException = new java.io.IOException("Checked error");
// You can check the hierarchy at runtime
System.out.println("RuntimeException is Exception: " +
(runtimeEx instanceof Exception));
System.out.println("IOException is Exception: " +
(checkedException instanceof Exception));
System.out.println("Exception is Throwable: " +
(checkedException instanceof Throwable));
// Get class hierarchy information
printClassHierarchy(IllegalArgumentException.class);
}
private static void printClassHierarchy(Class<?> clazz) {
System.out.println("\nClass hierarchy for " + clazz.getSimpleName() + ":");
Class<?> current = clazz;
int depth = 0;
while (current != null) {
System.out.println(" ".repeat(depth) + current.getName());
current = current.getSuperclass();
depth++;
}
}
}
Checked vs Unchecked Exceptions
The most important distinction in Java's exception system is between checked and unchecked exceptions. This distinction affects how you write code and handle errors.
Checked Exceptions
Checked exceptions represent recoverable conditions that a reasonable application might want to catch. They must be either caught or declared in the method signature.
Characteristics:
- Compile-time enforcement: Must be handled or declared
- Recoverable conditions: Situations the application can reasonably handle
- External dependencies: Often related to I/O, networking, or external systems
- Explicit handling: Forces developers to think about error scenarios
import java.io.*;
import java.sql.*;
import java.net.*;
public class CheckedExceptionExamples {
// Method that throws checked exceptions must declare them
public void readFileExample(String filename) throws IOException {
// IOException is checked - must be handled or declared
FileReader file = new FileReader(filename);
BufferedReader reader = new BufferedReader(file);
try {
String line = reader.readLine();
System.out.println("First line: " + line);
} finally {
reader.close(); // Also throws IOException
}
}
// Multiple checked exceptions can be declared
public void databaseExample(String url) throws SQLException, ClassNotFoundException {
// Both exceptions are checked
Class.forName("com.mysql.cj.jdbc.Driver"); // ClassNotFoundException
Connection conn = DriverManager.getConnection(url); // SQLException
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println("User: " + rs.getString("name"));
}
conn.close();
}
// Network operations throw checked exceptions
public String downloadContent(String urlString) throws IOException, MalformedURLException {
URL url = new URL(urlString); // MalformedURLException
URLConnection connection = url.openConnection(); // IOException
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream()))) {
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
return content.toString();
}
}
// Proper exception handling
public void handleCheckedExceptions() {
try {
readFileExample("data.txt");
} catch (FileNotFoundException e) {
System.err.println("File not found: " + e.getMessage());
// Could create default file or prompt user
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
// Could retry or use cached data
}
try {
String content = downloadContent("https://api.example.com/data");
System.out.println("Downloaded: " + content.length() + " characters");
} catch (MalformedURLException e) {
System.err.println("Invalid URL format: " + e.getMessage());
} catch (IOException e) {
System.err.println("Network error: " + e.getMessage());
// Could retry with exponential backoff
}
}
public static void main(String[] args) {
CheckedExceptionExamples example = new CheckedExceptionExamples();
example.handleCheckedExceptions();
}
}
Unchecked Exceptions (Runtime Exceptions)
Unchecked exceptions represent programming errors that could typically be prevented by proper coding practices. They don't need to be caught or declared.
Characteristics:
- No compile-time enforcement: Optional to handle
- Programming errors: Usually indicate bugs in the code
- Preventable: Can typically be avoided through careful programming
- Internal logic: Usually related to application logic rather than external factors
import java.util.*;
public class UncheckedExceptionExamples {
// Common runtime exceptions and how they occur
public void nullPointerExamples() {
System.out.println("=== NullPointerException Examples ===");
// Example 1: Null object method call
String str = null;
try {
int length = str.length(); // NullPointerException
} catch (NullPointerException e) {
System.out.println("NPE: Calling method on null object");
}
// Example 2: Null array access
int[] array = null;
try {
int value = array[0]; // NullPointerException
} catch (NullPointerException e) {
System.out.println("NPE: Accessing null array");
}
// Example 3: Null in collections
List<String> list = Arrays.asList("a", null, "c");
try {
list.stream()
.map(String::toUpperCase) // NPE on null element
.forEach(System.out::println);
} catch (NullPointerException e) {
System.out.println("NPE: Null element in stream operation");
}
}
public void illegalArgumentExamples() {
System.out.println("\n=== IllegalArgumentException Examples ===");
// Example 1: Invalid method parameters
try {
Thread.sleep(-1000); // Negative sleep time
} catch (IllegalArgumentException e) {
System.out.println("IAE: " + e.getMessage());
} catch (InterruptedException e) {
// InterruptedException is checked, IllegalArgumentException is not
}
// Example 2: Invalid enum value
try {
DayOfWeek.valueOf("FUNDAY"); // Invalid enum constant
} catch (IllegalArgumentException e) {
System.out.println("IAE: Invalid enum value");
}
// Example 3: Custom validation
try {
createUser("", -25); // Invalid user data
} catch (IllegalArgumentException e) {
System.out.println("IAE: " + e.getMessage());
}
}
public void arrayIndexExamples() {
System.out.println("\n=== ArrayIndexOutOfBoundsException Examples ===");
int[] numbers = {1, 2, 3, 4, 5};
try {
int value = numbers[10]; // Index beyond array bounds
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("AIOOBE: Array index " + e.getMessage());
}
try {
int value = numbers[-1]; // Negative index
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("AIOOBE: Negative array index");
}
// Similar with StringIndexOutOfBoundsException
String text = "Hello";
try {
char ch = text.charAt(10);
} catch (StringIndexOutOfBoundsException e) {
System.out.println("SIOOBE: String index out of bounds");
}
}
public void classCastExamples() {
System.out.println("\n=== ClassCastException Examples ===");
Object obj = "Hello World";
try {
Integer number = (Integer) obj; // Can't cast String to Integer
} catch (ClassCastException e) {
System.out.println("CCE: Cannot cast " +
obj.getClass().getSimpleName() + " to Integer");
}
// More subtle case with collections
List<Object> objects = new ArrayList<>();
objects.add("string");
objects.add(42);
for (Object o : objects) {
try {
String s = (String) o; // Fails on the Integer
System.out.println("String: " + s);
} catch (ClassCastException e) {
System.out.println("CCE: Object is " + o.getClass().getSimpleName());
}
}
}
public void numberFormatExamples() {
System.out.println("\n=== NumberFormatException Examples ===");
String[] inputs = {"123", "abc", "12.34", "", null};
for (String input : inputs) {
try {
int number = Integer.parseInt(input);
System.out.println("Parsed: " + number);
} catch (NumberFormatException e) {
System.out.println("NFE: Cannot parse '" + input + "' as integer");
} catch (NullPointerException e) {
System.out.println("NPE: Input is null");
}
}
}
// Custom method that validates arguments
private User createUser(String name, int age) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Age must be between 0 and 150");
}
return new User(name, age);
}
// Demonstration of preventing runtime exceptions
public void preventionExamples() {
System.out.println("\n=== Prevention Examples ===");
// Prevent NullPointerException
String possiblyNull = Math.random() > 0.5 ? "Hello" : null;
// Bad: Direct access without check
// int length = possiblyNull.length(); // Potential NPE
// Good: Null check
if (possiblyNull != null) {
int length = possiblyNull.length();
System.out.println("Safe length: " + length);
}
// Good: Using Optional
Optional.ofNullable(possiblyNull)
.map(String::length)
.ifPresent(len -> System.out.println("Optional length: " + len));
// Prevent ArrayIndexOutOfBoundsException
int[] array = {1, 2, 3};
int index = 5;
// Bad: Direct access without bounds check
// int value = array[index]; // Potential AIOOBE
// Good: Bounds check
if (index >= 0 && index < array.length) {
int value = array[index];
System.out.println("Safe access: " + value);
} else {
System.out.println("Index " + index + " is out of bounds");
}
// Prevent ClassCastException
Object obj = "Hello";
// Bad: Direct cast without check
// Integer num = (Integer) obj; // Potential CCE
// Good: instanceof check
if (obj instanceof Integer) {
Integer num = (Integer) obj;
System.out.println("Safe cast: " + num);
} else {
System.out.println("Object is not an Integer: " + obj.getClass().getSimpleName());
}
}
public static void main(String[] args) {
UncheckedExceptionExamples example = new UncheckedExceptionExamples();
example.nullPointerExamples();
example.illegalArgumentExamples();
example.arrayIndexExamples();
example.classCastExamples();
example.numberFormatExamples();
example.preventionExamples();
}
// Helper classes
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
enum DayOfWeek {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
}
Error Types
Errors represent serious problems that applications should not attempt to handle. They indicate system-level failures or resource exhaustion.
Common Error Types:
- OutOfMemoryError: JVM has run out of memory
- StackOverflowError: Method call stack has been exhausted
- NoClassDefFoundError: Class was available at compile time but not at runtime
- VirtualMachineError: JVM is broken or has run out of resources
public class ErrorExamples {
// These examples are for educational purposes only
// In real applications, you should prevent these conditions
public void demonstrateStackOverflowError() {
System.out.println("=== StackOverflowError Example ===");
try {
recursiveMethod(0);
} catch (StackOverflowError e) {
System.out.println("Caught StackOverflowError after deep recursion");
// In practice, you shouldn't catch Error types
}
}
private void recursiveMethod(int depth) {
// This will eventually cause StackOverflowError
// Don't do this in real code!
if (depth < 5) { // Limit for demo purposes
System.out.println("Recursion depth: " + depth);
recursiveMethod(depth + 1);
}
}
public void demonstrateOutOfMemoryError() {
System.out.println("=== OutOfMemoryError Example ===");
// This is dangerous and shouldn't be done in production!
// Included only for educational purposes
try {
// This will consume memory rapidly
List<String> memoryHog = new ArrayList<>();
int count = 0;
while (count < 1000) { // Limited for demo
// Create large strings
memoryHog.add("X".repeat(1000000)); // 1MB string
count++;
if (count % 100 == 0) {
System.out.println("Created " + count + " large strings");
}
}
} catch (OutOfMemoryError e) {
System.out.println("Caught OutOfMemoryError: " + e.getMessage());
// In practice, you should prevent this condition
// rather than trying to catch it
}
}
public void demonstrateNoClassDefFoundError() {
System.out.println("=== NoClassDefFoundError Example ===");
// This typically happens when:
// 1. Class was available at compile time but not at runtime
// 2. Static initialization fails
// 3. ClassPath issues
try {
// This might throw NoClassDefFoundError if the class
// was removed from classpath after compilation
Class.forName("some.missing.Class");
} catch (ClassNotFoundException e) {
System.out.println("ClassNotFoundException: " + e.getMessage());
} catch (NoClassDefFoundError e) {
System.out.println("NoClassDefFoundError: " + e.getMessage());
}
}
// Example of a class that might cause NoClassDefFoundError
static class ProblematicClass {
// If this static initialization fails, accessing this class
// will throw NoClassDefFoundError
static {
// Simulate initialization failure
if (Math.random() > 2.0) { // This will never happen
throw new RuntimeException("Static initialization failed");
}
}
}
public void errorHandlingGuidelines() {
System.out.println("=== Error Handling Guidelines ===");
System.out.println("1. Don't catch Error types in normal application code");
System.out.println("2. Use monitoring to detect Error conditions");
System.out.println("3. Design your application to prevent Error conditions");
System.out.println("4. Log Error occurrences for analysis");
System.out.println("5. Consider graceful shutdown when Errors occur");
// Example of appropriate Error handling in framework code
try {
// Some critical operation
performCriticalOperation();
} catch (OutOfMemoryError e) {
// Only in very specific cases, like servers that need
// to attempt graceful shutdown
System.err.println("Critical: Out of memory. Initiating shutdown.");
// Log the error
// Attempt to free resources
// Notify monitoring systems
// Shutdown gracefully
}
}
private void performCriticalOperation() {
// Simulate some operation
System.out.println("Performing critical operation...");
}
public static void main(String[] args) {
ErrorExamples examples = new ErrorExamples();
examples.demonstrateStackOverflowError();
examples.demonstrateOutOfMemoryError();
examples.demonstrateNoClassDefFoundError();
examples.errorHandlingGuidelines();
}
}
Common Exception Types in Detail
IOException and Subclasses
IOException represents failures in I/O operations and is one of the most common checked exceptions:
import java.io.*;
import java.net.*;
import java.nio.file.*;
public class IOExceptionExamples {
public void fileOperationExceptions() {
System.out.println("=== File I/O Exceptions ===");
// FileNotFoundException (subclass of IOException)
try {
FileReader reader = new FileReader("nonexistent.txt");
} catch (FileNotFoundException e) {
System.out.println("File not found: " + e.getMessage());
}
// General IOException
try {
Files.write(Paths.get("/restricted/file.txt"),
"content".getBytes());
} catch (AccessDeniedException e) {
System.out.println("Access denied: " + e.getMessage());
} catch (IOException e) {
System.out.println("I/O error: " + e.getMessage());
}
// EOFException (End of File)
try (DataInputStream dis = new DataInputStream(
new ByteArrayInputStream(new byte[]{1, 2}))) {
dis.readInt(); // Tries to read 4 bytes, only 2 available
} catch (EOFException e) {
System.out.println("Unexpected end of file");
} catch (IOException e) {
System.out.println("I/O error: " + e.getMessage());
}
}
public void networkExceptions() {
System.out.println("\n=== Network Exceptions ===");
// SocketTimeoutException
try {
URL url = new URL("http://httpbin.org/delay/10");
URLConnection conn = url.openConnection();
conn.setConnectTimeout(1000); // 1 second timeout
conn.setReadTimeout(1000);
try (InputStream in = conn.getInputStream()) {
// This will likely timeout
in.read();
}
} catch (SocketTimeoutException e) {
System.out.println("Connection timed out: " + e.getMessage());
} catch (IOException e) {
System.out.println("Network error: " + e.getMessage());
}
// UnknownHostException
try {
URL url = new URL("http://this-domain-does-not-exist-12345.com");
url.openConnection().connect();
} catch (UnknownHostException e) {
System.out.println("Unknown host: " + e.getMessage());
} catch (IOException e) {
System.out.println("Network error: " + e.getMessage());
}
// ConnectException
try {
Socket socket = new Socket("localhost", 99999); // Unlikely to be open
} catch (ConnectException e) {
System.out.println("Connection refused: " + e.getMessage());
} catch (IOException e) {
System.out.println("Socket error: " + e.getMessage());
}
}
public static void main(String[] args) {
IOExceptionExamples examples = new IOExceptionExamples();
examples.fileOperationExceptions();
examples.networkExceptions();
}
}
Collection-Related Exceptions
import java.util.*;
import java.util.concurrent.*;
public class CollectionExceptions {
public void listExceptions() {
System.out.println("=== List Exceptions ===");
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
// IndexOutOfBoundsException
try {
String item = list.get(10);
} catch (IndexOutOfBoundsException e) {
System.out.println("List index out of bounds: " + e.getMessage());
}
// ConcurrentModificationException
try {
for (String item : list) {
if ("b".equals(item)) {
list.remove(item); // Modifying while iterating
}
}
} catch (ConcurrentModificationException e) {
System.out.println("Concurrent modification detected");
}
// Proper way to remove during iteration
list.removeIf("b"::equals);
System.out.println("List after safe removal: " + list);
}
public void mapExceptions() {
System.out.println("\n=== Map Exceptions ===");
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
// No exception for missing key (returns null)
Integer value = map.get("three");
System.out.println("Missing key returns: " + value);
// But this might cause NullPointerException if you don't check
try {
int primitiveValue = map.get("three"); // Auto-unboxing null
} catch (NullPointerException e) {
System.out.println("NPE: Auto-unboxing null Integer");
}
// Safe way
int safeValue = map.getOrDefault("three", 0);
System.out.println("Safe access with default: " + safeValue);
}
public void queueExceptions() {
System.out.println("\n=== Queue Exceptions ===");
Queue<String> queue = new ArrayDeque<>();
// NoSuchElementException
try {
String item = queue.remove(); // Queue is empty
} catch (NoSuchElementException e) {
System.out.println("Queue is empty: " + e.getMessage());
}
// Alternative that returns null instead of throwing
String item = queue.poll();
System.out.println("Poll on empty queue returns: " + item);
// element() vs peek()
try {
String head = queue.element(); // Throws if empty
} catch (NoSuchElementException e) {
System.out.println("element() on empty queue throws exception");
}
String head = queue.peek(); // Returns null if empty
System.out.println("peek() on empty queue returns: " + head);
}
public void concurrentCollectionExceptions() {
System.out.println("\n=== Concurrent Collection Exceptions ===");
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(2);
// IllegalStateException when queue is full
blockingQueue.offer("item1");
blockingQueue.offer("item2");
try {
blockingQueue.add("item3"); // Queue is full
} catch (IllegalStateException e) {
System.out.println("Queue is full: " + e.getMessage());
}
// offer() returns false instead of throwing
boolean added = blockingQueue.offer("item3");
System.out.println("offer() on full queue returns: " + added);
}
public static void main(String[] args) {
CollectionExceptions examples = new CollectionExceptions();
examples.listExceptions();
examples.mapExceptions();
examples.queueExceptions();
examples.concurrentCollectionExceptions();
}
}
Best Practices for Exception Types
When to Use Each Type
public class ExceptionBestPractices {
// Use checked exceptions for recoverable conditions
public void processFile(String filename) throws IOException {
// Caller can handle this by providing a different file,
// creating the file, or using a default
if (!Files.exists(Paths.get(filename))) {
throw new FileNotFoundException("File not found: " + filename);
}
// Process the file
}
// Use unchecked exceptions for programming errors
public void divide(int dividend, int divisor) {
if (divisor == 0) {
// This is a programming error - caller should check before calling
throw new IllegalArgumentException("Division by zero");
}
int result = dividend / divisor;
System.out.println("Result: " + result);
}
// Use specific exception types when possible
public void validateUserInput(String username, String password) {
if (username == null) {
throw new NullPointerException("Username cannot be null");
}
if (username.trim().isEmpty()) {
throw new IllegalArgumentException("Username cannot be empty");
}
if (password == null) {
throw new NullPointerException("Password cannot be null");
}
if (password.length() < 8) {
throw new IllegalArgumentException("Password must be at least 8 characters");
}
}
// Don't catch exceptions you can't handle meaningfully
public void goodExceptionHandling() {
try {
processFile("data.txt");
} catch (FileNotFoundException e) {
// Can handle this - use default file
System.out.println("Using default configuration");
useDefaultConfiguration();
} catch (IOException e) {
// Can handle this - retry or fail gracefully
System.err.println("I/O error, retrying...");
retryOperation();
}
}
// Don't catch and ignore exceptions
public void badExceptionHandling() {
try {
processFile("data.txt");
} catch (IOException e) {
// BAD: Silently ignoring the exception
// The error condition still exists but is hidden
}
}
// Prefer specific catch blocks over generic ones
public void specificCatchBlocks() {
try {
validateUserInput("john", "pass");
divide(10, 0);
} catch (NullPointerException e) {
System.err.println("Null value provided: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.err.println("Invalid argument: " + e.getMessage());
}
// Don't catch RuntimeException or Exception generically
// unless you really need to handle all types the same way
}
// Use exception chaining to preserve information
public void processUserData(String data) throws DataProcessingException {
try {
// Some complex processing
parseAndValidateData(data);
} catch (NumberFormatException e) {
// Chain the original exception to preserve stack trace
throw new DataProcessingException("Failed to parse user data", e);
} catch (IllegalArgumentException e) {
throw new DataProcessingException("Invalid user data format", e);
}
}
private void parseAndValidateData(String data) {
// Simulate parsing that might fail
Integer.parseInt(data); // Might throw NumberFormatException
}
private void useDefaultConfiguration() {
System.out.println("Loading default configuration...");
}
private void retryOperation() {
System.out.println("Retrying operation...");
}
// Custom exception for chaining example
public static class DataProcessingException extends Exception {
public DataProcessingException(String message) {
super(message);
}
public DataProcessingException(String message, Throwable cause) {
super(message, cause);
}
}
public static void main(String[] args) {
ExceptionBestPractices examples = new ExceptionBestPractices();
// Demonstrate good practices
examples.goodExceptionHandling();
examples.specificCatchBlocks();
try {
examples.processUserData("invalid");
} catch (DataProcessingException e) {
System.err.println("Data processing failed: " + e.getMessage());
System.err.println("Caused by: " + e.getCause().getClass().getSimpleName());
}
}
}
Summary
Understanding Java's exception types is crucial for writing robust applications:
Exception Hierarchy:
- Throwable: Root of all exceptions and errors
- Error: System-level problems (don't catch these)
- Exception: Application-level problems (handle these)
- RuntimeException: Programming errors (optional to catch)
- Checked Exceptions: Expected conditions (must handle)
Key Principles:
- Checked exceptions for recoverable external conditions
- Unchecked exceptions for programming errors
- Errors for system-level failures (don't catch)
- Specific types provide better error handling context
Best Practices:
- Use the most specific exception type appropriate
- Handle exceptions you can meaningfully recover from
- Don't catch and ignore exceptions
- Use exception chaining to preserve context
- Prefer prevention over exception handling for runtime exceptions
- Document exceptions in method signatures and JavaDoc
Common Patterns:
- I/O operations: Checked exceptions (IOException, FileNotFoundException)
- Network operations: Checked exceptions (SocketException, UnknownHostException)
- Validation failures: Unchecked exceptions (IllegalArgumentException)
- Null references: Unchecked exceptions (NullPointerException)
- Index/bounds errors: Unchecked exceptions (IndexOutOfBoundsException)
Proper exception handling makes your applications more reliable, maintainable, and provides better user experiences by gracefully handling error conditions.