Alexander Claes
← Learning Java

Handling Exceptions in Java

Learn how Java exceptions work, when to catch or declare them, and how to build a temperature converter that handles invalid input without crashing.

Alexander Claes Alexander Claes 5 min read
Learning Java
A safety net catches a code block falling through a broken path, with a thermometer in the background.

A program is easy to follow when every input is valid and every operation succeeds. Real input is less predictable: someone types a word where a number belongs, or a file cannot be opened. Exception handling gives us a way to respond deliberately when an operation fails.

Building on reading and writing in Java, this article explores how exceptions travel through a program, how to catch them, and what checked exceptions require. We will finish with a more resilient temperature converter. The examples use the traditional class-and-main structure and APIs available in Java 17 and later.

How errors work

A compilation error, such as a missing semicolon, prevents code from compiling. An exception happens while the program is running. A logic mistake is different again: a program can run successfully and still calculate the wrong answer.

int temperature = Integer.parseInt("warm");
System.out.println("Conversion complete.");

The first statement throws NumberFormatException. Without a matching handler, execution does not continue to the next statement. Java searches outward through the active method calls for a catch block that accepts the exception. If none handles it, the current thread terminates; in a simple single-threaded console program, that ends the program.

An exception is an object with a type, a message, and diagnostic information. A stack trace tells you where it was thrown and which method calls led there. Read the exception type and message first, then look for the first relevant line in your own code. A Caused by section can reveal an underlying failure.

Catching exceptions

try {
    int temperature = Integer.parseInt("warm");
    System.out.println("Temperature: " + temperature);
} catch (NumberFormatException exception) {
    System.out.println("Please enter a whole number.");
}
System.out.println("The program can continue.");

The try block contains the operation that might fail. The catch block runs only when a compatible exception reaches it. Here, the temperature output is skipped, the friendly message is printed, and execution continues after the catch block. Catching does not restart the failed statement.

Catch the specific failures you know how to handle. Wrapping an entire program in catch (Exception exception) can disguise unrelated bugs. If multiple catch blocks are needed, place more specific types before their parent types. Keep the protected section small enough that its recovery behavior is clear.

Do something useful in a handler: ask for another value, explain why an operation failed, or pass the failure to a caller that can decide. An empty catch block simply hides the problem. The official catching and handling guide explains the structure in more detail.

Two kinds of exceptions

The practical distinction is checked versus unchecked. Both happen at runtime; “checked” describes what the compiler requires you to do about the possibility of a failure.

Checked exceptions, such as IOException, must be caught or declared in the enclosing method’s throws clause when they can propagate from that method. A file operation is a common example: even correct code can encounter a missing file or denied access.

Unchecked exceptions include RuntimeException and its subclasses, such as NumberFormatException, IllegalArgumentException, and NullPointerException. The compiler does not require a catch or throws declaration. That does not mean they should always be ignored: invalid user input is often worth handling, while a null dereference caused by a bug should usually be fixed at its source.

Error is another branch under Throwable, separate from Exception. Errors such as OutOfMemoryError are unchecked too, but ordinary application code generally should not try to recover from them with a blanket handler. See the official exception overview for the hierarchy and catch-or-declare rule.

Dealing with checked exceptions

Suppose a helper reads a saved temperature from a file. It can leave the decision about failure to its caller by declaring IOException:

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

public class SavedTemperature {
    static String readTemperature(Path path) throws IOException {
        return Files.readString(path).trim();
    }

    public static void main(String[] args) {
        try {
            String value = readTemperature(Path.of("temperature.txt"));
            System.out.println("Saved temperature: " + value);
        } catch (IOException exception) {
            System.err.println("Could not read temperature.txt.");
            System.err.println(exception.getMessage());
        }
    }
}

The helper declares the failure; main handles it. throws IOException is not recovery code, and it does not force an exception to happen. It tells callers what may escape the method. By contrast, throw actually throws an exception object.

static double toFahrenheit(double celsius) {
    if (!Double.isFinite(celsius)) {
        throw new IllegalArgumentException("Temperature must be finite.");
    }
    return celsius * 1.8 + 32;
}

This method fragment illustrates an unchecked exception for an invalid argument. It does not need a throws declaration. Whether a method declares or catches a checked exception should depend on where a meaningful response can be made—not simply on silencing the compiler.

A more solid temperature converter

This version asks for one Celsius value, then either displays the Fahrenheit result or explains why the input cannot be used. It uses try/catch and simple if checks, without introducing loops.

import java.util.Scanner;

public class TemperatureConverter {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Celsius: ");
        System.out.flush();

        if (!input.hasNextLine()) {
            System.out.println("No temperature entered.");
            return;
        }

        String text = input.nextLine().trim();
        double celsius;

        try {
            celsius = Double.parseDouble(text);
        } catch (NumberFormatException exception) {
            System.out.println("Please enter a number such as 21.5.");
            return;
        }

        if (!Double.isFinite(celsius)) {
            System.out.println("Please enter a finite temperature.");
            return;
        }

        if (celsius < -273.15) {
            System.out.println("Celsius cannot be below -273.15.");
            return;
        }

        double fahrenheit = celsius * 1.8 + 32;
        if (!Double.isFinite(fahrenheit)) {
            System.out.println("That value is too large to convert.");
            return;
        }

        System.out.println(celsius + " C = " + fahrenheit + " F");
    }
}

Run the main method in IntelliJ IDEA to try the converter.

try/catch handles text that cannot be parsed as a number; the if checks reject numbers that are unsuitable for the conversion. Each invalid case prints a message and exits. Enter decimals with a point, such as 21.5.

The idea to keep

Exception handling is not about making failures disappear. It is about choosing a useful response at the right boundary. Catch the failures you can handle, declare checked exceptions when the caller should decide, and validate values separately from parsing them. A small program becomes much more dependable once its failure paths receive the same attention as its successful path.