You are currently viewing Java Exception Handling for Beginners: try, catch, throws, and Stack Traces

Java Exception Handling for Beginners: try, catch, throws, and Stack Traces

A program may compile without errors and still fail when it tries to open a missing file, convert invalid text to a number, or access an array index that does not exist. Java represents these runtime problems as objects, so your code can respond in a controlled way instead of producing unreliable results or stopping without explanation.

An exception is an event that interrupts the normal flow of instructions. When Java encounters one, it creates an exception object and looks for code that can handle it. If no suitable handler is available, execution stops and Java prints a stack trace: a record of the method calls that led to the failure.

Errors, exceptions, and why the distinction matters

Java uses the Throwable hierarchy for problems that can be thrown and caught. Its two main branches are Error and Exception.

  • Errors usually indicate serious JVM or system-level problems, such as OutOfMemoryError. Application code generally should not try to recover from them.
  • Exceptions represent conditions an application can often anticipate or handle, such as invalid input, unavailable files, or a failed network connection.

Exceptions fall into two practical groups. Checked exceptions must be caught or declared to the compiler. IOException is a common example. Unchecked exceptions extend RuntimeException, so the compiler does not require you to handle them. Examples include NullPointerException, IllegalArgumentException, and IndexOutOfBoundsException.

Type Compiler requirement Typical meaning
Checked exception Catch it or declare it with throws An external operation may fail
Unchecked exception No required declaration A violated precondition or programming defect
Error No required declaration A severe problem usually outside normal recovery

Checked and unchecked does not mean minor and serious. Invalid user input may be important even when it causes an unchecked exception. The distinction mainly tells you whether callers must explicitly acknowledge the possible failure.

A developer reviews an exception trace

Reading a stack trace without guessing

A stack trace can look overwhelming because it includes framework and library calls. Usually, the most useful clues are near the top. Consider this example:

Exception in thread "main" java.lang.NumberFormatException: For input string: "twelve"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:668)
    at example.App.main(App.java:12)

Start with the exception class and message. NumberFormatException tells you that text expected to represent an integer was invalid. Then look for the first line that refers to your own package or source file. Here, App.java:12 points to the line that called Integer.parseInt(). The library lines show how Java reached the failure, but your code is usually the first place to investigate.

Do not treat a stack trace as something to hide. During development, it is evidence. Reproduce the input, find the first application frame, inspect the values reaching that line, and decide whether the input should be rejected, corrected, or handled another way.

Using try, catch, and finally

Put an operation that may fail inside a try block. A matching catch block receives the exception and defines what the program should do next.

String enteredAge = "twelve";

try {
    int age = Integer.parseInt(enteredAge);
    System.out.println("Age: " + age);
} catch (NumberFormatException e) {
    System.out.println("Enter a whole-number age.");
}

Once parseInt() fails, the program does not continue through the rest of the try block. Control moves directly to the matching catch. Code after a risky operation should therefore not assume that operation completed.

Catch the most specific type first

A catch block for a parent class also catches its child classes. For that reason, Java requires specific exceptions to appear before broader ones:

try {
    // file operation
} catch (java.io.FileNotFoundException e) {
    System.out.println("The selected file was not found.");
} catch (java.io.IOException e) {
    System.out.println("The file could not be read.");
}

Reversing these blocks produces a compilation error because FileNotFoundException is already covered by IOException. Avoid catching Exception unless the same recovery action genuinely applies to every exception in that group. A broad catch can hide defects that need attention.

A finally block runs after the try block and any matching catch, whether the operation succeeds or fails. It was traditionally used for cleanup:

try {
    System.out.println("Processing request");
} finally {
    System.out.println("Cleanup code runs here");
}

Be careful with finally. Returning from it or throwing a new exception there can conceal the original failure and make debugging much harder.

Prefer try-with-resources for files and streams

Files, network connections, and database-related objects often hold resources that must be closed. Try-with-resources closes declared resources automatically when the block ends, including when an exception occurs.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

try (BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))) {
    String firstLine = reader.readLine();
    System.out.println(firstLine);
} catch (IOException e) {
    System.out.println("Could not read notes.txt: " + e.getMessage());
}

This approach is clearer and less error-prone than closing the reader manually in finally. The resource must implement AutoCloseable, which many Java I/O classes already do. Keep messages shown to end users helpful but limited. Detailed paths, configuration values, and internal service information may belong in protected logs, but they should not be exposed unnecessarily in production applications.

Try-with-resources keeps file handling concise

When to use throws instead of catching

A method does not always have enough context to recover from a failure. It can declare that a checked exception may leave the method by using throws:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

static String loadSettings(Path path) throws IOException {
    return Files.readString(path);
}

This does not handle the exception. It passes responsibility to the caller, which may be better positioned to retry, choose another path, notify the user, or stop the operation. At an application boundary, such as a command-line entry point, UI controller, or web request handler, catch the exception and convert it into a response that fits that environment.

Do not add throws Exception simply to silence compiler errors. Declare the narrowest meaningful type, such as IOException, so callers know what can go wrong.

Creating exceptions that explain a failed rule

Custom exceptions are useful when a failure belongs to your program's domain rather than one of Java's built-in categories. For example, a training application might reject an account name that breaks its rules:

class InvalidUsernameException extends Exception {
    InvalidUsernameException(String message) {
        super(message);
    }
}

static void validateUsername(String username) throws InvalidUsernameException {
    if (username == null || username.length() < 3) {
        throw new InvalidUsernameException("Username must contain at least three characters.");
    }
}

Use a checked custom exception when callers are expected to make a deliberate recovery decision. Extend RuntimeException when the condition usually signals incorrect method use or an invalid internal state. Choose names that describe the business problem rather than a vague technical event: InsufficientBalanceException says more than OperationFailedException.

Common beginner mistakes

  • Using exceptions for ordinary branching. Check predictable conditions first when practical, such as testing whether text is blank before parsing it.
  • Swallowing an exception. An empty catch block makes failures invisible and can leave the program in an uncertain state.
  • Printing only a vague message. During development, preserve exception details with logging or e.printStackTrace(). Production systems need structured, access-controlled logs.
  • Catching NullPointerException as routine logic. Find and fix the unexpected null value, or validate inputs before using them.
  • Changing an exception into an unrelated one. When wrapping an exception, preserve the cause with a constructor such as new IllegalStateException("Could not load settings", e).

Exception handling does not replace validation. Validate data where it enters the program, use clear exception types when an operation cannot continue, and catch exceptions only when your code has a useful recovery action to perform.

For practice, write a method that reads a line of text and converts it to an integer. Catch NumberFormatException, display a clear retry message, and test 42, an empty string, and 42.5. Each invalid value should lead to the same controlled result rather than terminating the program.