Java Exception Handling — Complete Guide
In this tutorial, you'll learn about Java Exception Handling. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Java Exception Handling uses try-catch blocks and a typed throwable hierarchy to recover from errors without crashing or leaking sensitive information.
What You'll Learn
You'll understand the exception hierarchy (checked vs unchecked vs errors), write try-catch-finally blocks, create custom exceptions, use try-with-resources for automatic cleanup, and apply Exception Handling patterns used in production systems. You'll also learn why proper Exception Handling is a security concern.
Why Exception Handling Matters
Unhandled exceptions crash applications, corrupt data, and leak sensitive information through stack traces. In security-sensitive systems — like DodaTech's Durga Antivirus Pro — an unhandled exception during a scan could leave the system in an inconsistent state, skip a threat, or expose internal paths in error messages shown to users. Proper Exception Handling is not just good practice — it's a security requirement.
Real-World Use: File Scanner Error Recovery
Imagine an antivirus scanner processing 100,000 files. One corrupted file shouldn't crash the entire scan. With proper Exception Handling, the scanner logs the error, skips the problematic file, and continues. DodaTech's scan engine catches IOException around each file read, logs the path and error, and increments an error counter — the scan completes with a summary showing 1 error out of 100,000 files.
Exception Handling Learning Path
flowchart LR A[Java Basics] --> B[Java OOP] B --> C[Java Exception Handling] C --> D[Java Collections] C --> E[Java Lambdas & Streams] D --> F[File I/O & JDBC] E --> G[Spring Boot] D --> H[Java Concurrency] C:::current classDef current fill:#3b82f6,color:#fff,stroke:#333,stroke-width:2px
Understanding the Exception Hierarchy
All exceptions in Java are subclasses of Throwable. The hierarchy splits into three categories:
| Category | Base Class | Example | Must Handle? |
|---|---|---|---|
| Checked | Exception (excluding RuntimeException) |
IOException, SQLException |
Yes — compiler forces try-catch or throws |
| Unchecked | RuntimeException |
NullPointerException, IllegalArgumentException |
No — but should be prevented or caught |
| Errors | Error |
OutOfMemoryError, StackOverflowError |
No — usually unrecoverable |
Checked exceptions represent external failures you can recover from — like a missing file or network timeout. Unchecked exceptions represent programming bugs — like accessing a null reference or dividing by zero. Errors represent JVM-level failures that the application typically cannot handle.
Try-Catch-Finally — The Basics
import java.io.*;
public class ExceptionBasics {
public static void main(String[] args) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("config.properties"));
String line = reader.readLine();
System.out.println("First config line: " + line);
} catch (FileNotFoundException e) {
System.out.println("ERROR: Config file not found — using defaults");
System.out.println("Details: " + e.getMessage());
} catch (IOException e) {
System.out.println("ERROR: Failed to read config file");
System.err.println("Cause: " + e.getCause());
} finally {
// Always executes — even if an exception is thrown or caught
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
System.err.println("WARNING: Failed to close reader");
}
}
System.out.println("Application continues normally");
}
}
Expected output (when config.properties is missing):
ERROR: Config file not found — using defaults
Details: config.properties (No such file or directory)
Application continues normally
The finally block is where you put cleanup code — closing files, releasing network connections, or freeing native resources. It always runs, whether an exception was thrown or not.
Try-With-Resources — Automatic Cleanup
Java 7 introduced try-with-resources, which automatically closes resources that implement AutoCloseable. This eliminates the verbose finally block and prevents resource leaks.
import java.io.*;
import java.nio.file.*;
public class TryWithResourcesDemo {
public static void main(String[] args) {
Path logFile = Paths.get("server.log");
// Try-with-resources — resources are closed automatically
try (BufferedReader reader = Files.newBufferedReader(logFile);
BufferedWriter writer = Files.newBufferedWriter(
Paths.get("server-filtered.log"))) {
String line;
int errorCount = 0;
while ((line = reader.readLine()) != null) {
if (line.contains("ERROR") || line.contains("FATAL")) {
writer.write(line);
writer.newLine();
errorCount++;
}
}
System.out.println("Filtered " + errorCount + " error lines");
} catch (NoSuchFileException e) {
System.out.println("ERROR: Log file not found at " + logFile.toAbsolutePath());
} catch (IOException e) {
System.out.println("ERROR: Failed processing log file: " + e.getMessage());
}
// Resources (reader, writer) are already closed — no finally needed
}
}
Expected output (with server.log containing errors):
Filtered 12 error lines
Security benefit: Try-with-resources ensures file handles are always released, even if an exception occurs mid-operation. Leaked file handles can cause "too many open files" errors, which in security tools could mean missing critical logs during an incident. DodaTech uses try-with-resources in every file operation across its tools.
Checked vs Unchecked — When to Use What
import java.io.*;
public class CheckedVsUnchecked {
public static void main(String[] args) {
// Unchecked — compiler does NOT require handling
String input = getUserInput();
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");
}
// Checked — compiler FORCES handling
try {
writeAuditLog(input);
} catch (IOException e) {
System.err.println("Audit failed: " + e.getMessage());
// In security tools, always log audit failures
}
}
static String getUserInput() {
// Simulate user input — could be null
return "Device scan initiated";
}
// 'throws' declares that this method can throw a checked exception
static void writeAuditLog(String message) throws IOException {
try (FileWriter fw = new FileWriter("audit.log", true);
BufferedWriter bw = new BufferedWriter(fw)) {
bw.write(java.time.LocalDateTime.now() + " — " + message);
bw.newLine();
}
}
}
Expected output (audit.log):
2026-06-20T10:30:00 — Device scan initiated
Rule of thumb: Use checked exceptions for recoverable conditions that callers should handle (file not found, network timeout). Use unchecked exceptions for programming errors that shouldn't happen (null arguments, invalid indices).
Creating Custom Exceptions
Custom exceptions make your code more readable and allow callers to catch specific error types. DodaTech's services use custom exceptions to distinguish between "device not found" (known state) and "database connection failed" (infrastructure issue).
// Custom checked exception
class ScanTimeoutException extends Exception {
public ScanTimeoutException(String deviceId, long timeoutMs) {
super("Scan timed out for device " + deviceId + " after " + timeoutMs + "ms");
this.deviceId = deviceId;
this.timeoutMs = timeoutMs;
}
private final String deviceId;
private final long timeoutMs;
public String getDeviceId() { return deviceId; }
public long getTimeoutMs() { return timeoutMs; }
}
// Custom unchecked exception
class InvalidConfigException extends RuntimeException {
public InvalidConfigException(String key, String value) {
super("Invalid configuration: " + key + " = " + value);
}
}
// Usage
public class CustomExceptionDemo {
public static void main(String[] args) {
try {
scanDevice("Sensor-42");
} catch (ScanTimeoutException e) {
System.err.println("Timeout on " + e.getDeviceId());
System.err.println("Duration: " + e.getTimeoutMs() + "ms");
// Retry logic would go here
}
}
static void scanDevice(String deviceId) throws ScanTimeoutException {
String configValue = "8080"; // Simulated config
if (!configValue.matches("\\d+")) {
throw new InvalidConfigException("port", configValue);
}
// Simulate scan that might time out
boolean timedOut = Math.random() > 0.5;
if (timedOut) {
throw new ScanTimeoutException(deviceId, 5000);
}
System.out.println("Scan completed for " + deviceId);
}
}
Expected output (one possible run):
Timeout on Sensor-42
Duration: 5000ms
Exception Handling Best Practices for Security
import java.io.*;
import java.sql.*;
public class SafeExceptionHandling {
public static void main(String[] args) {
try {
processFile("/etc/passwd");
} catch (Exception e) {
// BAD — exposes internal path in production
// e.printStackTrace();
// GOOD — log full details internally, show safe message to user
logError("File processing failed", e);
System.out.println("An error occurred while processing the file");
}
}
static void processFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String content = br.readLine();
// Process content...
}
}
static void logError(String message, Throwable cause) {
// In production, write to a secure, access-controlled log
System.err.println(java.time.LocalDateTime.now() + " — " + message);
System.err.println(" Exception: " + cause.getClass().getName());
System.err.println(" Message: " + cause.getMessage());
// Do NOT log the full stack trace to user-facing output
}
}
Expected output:
An error occurred while processing the file
Security rules for Exception Handling:
- Never print stack traces to user-facing output (exposes internal paths and architecture)
- Log full exception details to secure, access-controlled logs
- Show user-friendly, generic error messages
- Don't catch
Exceptionbroadly unless you're at the top level — catch specific types - Validate and sanitize exception messages that might contain user input
What Is the Difference Between throw and throws?
throw is a keyword used to actually throw an exception object: throw new IOException("message"). throws is a declaration in the method signature that tells callers "this method may throw this exception": void readFile() throws IOException. Use throw to raise exceptions; use throws to declare them.
Common Mistakes Beginners Make
Catching
ExceptionorThrowabletoo broadly:catch (Exception e)catches everything, includingRuntimeExceptionsubclasses you probably want to fail on. Always catch the most specific exception type.Swallowing exceptions with empty catch blocks:
catch (IOException e) {}hides errors and makes debugging impossible. At minimum, log the exception.Not closing resources in
finally: Before Java 7, forgetting to close resources infinallycaused resource leaks. Use try-with-resources to prevent this automatically.Throwing
Exceptionin method signatures: Declaringthrows Exceptionforces all callers to handle or re-declare it, making the API painful to use. Throw specific types.Using exceptions for normal control flow: Throwing exceptions for expected conditions (like "user not found") is expensive and confusing. Use return values or
Optionalfor expected cases.Logging and rethrowing the same exception:
catch (IOException e) { log(e); throw e; }produces duplicate log entries. Either handle it or throw it — not both.Exposing sensitive data in exception messages: Including file paths, SQL queries, or user data in exception messages can leak information to attackers through error responses.
Practice Questions
- What is the difference between checked and unchecked exceptions?
- What does
finallyguarantee? - How does try-with-resources work?
- When should you create a custom exception?
- What happens if both
tryandfinallythrow exceptions?
Answers:
- Checked exceptions must be handled or declared (compiler-enforced). Unchecked exceptions (subclasses of
RuntimeException) don't require handling — they represent programming errors. finallyalways executes aftertry/catch, regardless of whether an exception was thrown or caught. It runs even if there's areturnstatement.- Any resource implementing
AutoCloseabledeclared in thetryparentheses is automatically closed when the block exits, in reverse declaration order, without needing an explicitfinally. - Create custom exceptions when you need to convey specific error info (like error codes or context) and when callers need to handle different failure types differently.
- The
finallyblock's exception propagates, and thetryblock's exception is suppressed. UseaddSuppressed()to preserve both, or check the documentation for suppressed Exception Handling.
Challenge
Build a configuration file parser that reads key=value pairs from a file, validates each value according to its expected type (port numbers must be 1-65535, IP addresses must be valid, boolean values must be true/false), and throws custom exceptions (ConfigParseException, ValidationException) with clear messages showing the file, line number, and issue. Include a summary at the end showing how many lines succeeded and how many failed.
Real-World Task
Extend DodaTech's simulated log analyzer to handle partial failures gracefully. Create a batch log processor that reads 1,000 log entries, processes each one, and catches exceptions per entry so one corrupt entry doesn't stop the entire batch. Track success/failure counts and generate a processing report. This is the same pattern used in DodaTech's security log ingestion pipeline.
FAQ
What's Next
Related topics: Java, Java OOP, Java Collections, JUnit, Spring
Remember: exceptions are your friends — they tell you exactly what went wrong and where. Learn to read stack traces as well as you write code, and your debugging time will drop by 80%.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro