Skip to content
Ameen Abozaid
PublishedBEG-029-L08

Demo: finding a swallowed exception

  • Lesson 8 of 8
  • 12 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

This second demo is about the worst thing a program can do with an alarm: turn it off and say nothing. A swallowed exception is one that was caught and then ignored, usually with an empty catch block. The program does not crash and does not print anything. It just quietly does the wrong thing, and somebody finds out days later when the data is missing.

You will follow the steps of a real bug hunt: run a program that "does nothing" when a file is missing, find the empty catch, try the tempting half-fix of printing the stack trace, and then apply the proper fix. At the end you will have a short checklist you can use whenever a piece of code is handed to you with the question "why does this silently fail?". This is exactly the skill the module's review exercise tests.

Why this matters: swallowed exceptions are among the most expensive bugs in real systems, because nothing points at them. You can only find them by reading the code with the right suspicion, and by never writing them yourself.

This lesson builds on lessons 3 and 5 (narrow catches, keeping the cause), lesson 6 (files), and the debugging method from BEG-018.

A preview note. We use one more file call, Files.readString(path), which reads a whole small file into a String and throws IOException if it cannot. Treat it as a black box.

Learning Objectives

By the end of this lesson you will be able to:

  • Recognize a swallowed exception in code and by its symptoms.
  • Explain why catch (Exception e) {} is harmful, and why only printing the stack trace is not enough.
  • Fix it by catching the narrow type, giving the user a clear message, and keeping the cause when rethrowing.
  • Choose between recovering, reporting, and rethrowing.
  • Verify the fix by re-running the failing scenario.
  • Recognize the rare cases where ignoring an exception is acceptable, and how to mark them.

Written Explanation

The symptom

A swallowed exception shows up as absence: no error, no crash, and wrong or empty results. Typical reports sound like:

  • "The welcome screen shows no name."
  • "The report is blank, but there was no error."
  • "Some orders never arrive, and the logs are clean."

Compare with lesson 1: an unhandled exception is loud and tells you the type, message, and line. A swallowed one has been silenced on purpose, so the usual starting point of reading the trace is missing. You have to look for the place where the trace would have come from.

The hunt, step by step

  1. Reproduce. Run the program in the failing situation (here: the file is missing). Note exactly what you see, and what you expected to see.
  2. Narrow down. Find the code that produces the wrong value. Work backward from the visible symptom: where does the empty name come from?
  3. Search for suspects. Look for catch blocks near it, especially those with an empty body, with only a comment, with a return of a default value, or catching Exception or Throwable. Your editor's search or the command line can find the word catch across the project.
  4. Confirm. Temporarily print something in the block, or step through with the debugger (BEG-018). If it runs in your failing scenario, you have the culprit.
  5. Fix it properly (below), and re-run the same scenario to see the problem reported.

The program under suspicion

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

public class V0 {
    static String loadName(Path path) {
        try {
            return Files.readString(path).trim();
        } catch (Exception e) {
        }
        return "";
    }

    public static void main(String[] args) {
        String name = loadName(Path.of("user.txt"));
        System.out.println("Welcome, " + name + "!");
    }
}

With no user.txt in the folder, real output:

text
Welcome, !

No error and no clue. The empty catch swallowed a NoSuchFileException, and the method returned "" as if nothing had happened. The caller cannot tell "the user's name is empty" from "the file is missing".

Why the empty catch is dangerous

It does three bad things at once:

  • It hides the problem from the user and from the logs.
  • It hands a wrong value onward (""), which can corrupt later results, so the damage appears far from the cause.
  • It catches Exception, so it would also hide bugs that have nothing to do with files, such as a NullPointerException in the same try.

The tempting half-fix: printStackTrace only

The quickest "fix" is to fill the block with e.printStackTrace();. It is better than silence, but still wrong in a program that must work. Here is a truncated real run (the lines inside the JDK vary with the operating system and version, so some are replaced by ...):

text
java.nio.file.NoSuchFileException: user.txt
	at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85)
	...
	at java.base/java.nio.file.Files.readString(Files.java:3346)
	at V1.loadName(V1.java:7)
	at V1.main(V1.java:15)
Welcome, !

What is wrong with it?

  • The program still carries on and still prints Welcome, !. The caller is still handed a wrong answer.
  • The trace goes to the error output, which a real user never sees, and in a server it may go to a place nobody checks.
  • It still catches the broad Exception.
  • It looks like handling, so a reviewer may stop worrying about it.

Printing the trace is a debugging aid, not a plan.

The proper fix

A good catch block does at least one of three things, on purpose:

ChoiceWhenExample
RecoverA sensible default or retry exists and the caller is not misledUse a default setting and tell the user
ReportThe user or log must know, and this level can speak to themPrint a clear message naming the file
RethrowThis level cannot decide; pass a clearer exception up, with the causethrow new IllegalStateException("...", e)

And it uses the narrowest type, here IOException, instead of Exception. Applying that to our program:

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

public class V2 {
    static String loadName(Path path) {
        try {
            return Files.readString(path).trim();
        } catch (IOException e) {
            throw new IllegalStateException("Could not load the user name from " + path, e);
        }
    }

    public static void main(String[] args) {
        try {
            String name = loadName(Path.of("user.txt"));
            System.out.println("Welcome, " + name + "!");
        } catch (IllegalStateException e) {
            System.out.println("Cannot start: " + e.getMessage());
            System.out.println("Reason: " + e.getCause());
        }
    }
}

Real output with no file, then with a user.txt containing Sara:

text
Cannot start: Could not load the user name from user.txt
Reason: java.nio.file.NoSuchFileException: user.txt
Welcome, Sara!

loadName cannot decide what the user sees, so it rethrows a clearer exception with the original as cause, and main (the code that talks to the user) reports it. The wrong value "" no longer flows onward, and other kinds of exceptions (bugs) are not hidden anymore because the catch is narrow.

When ignoring an exception is acceptable

Rarely, ignoring is right: for example when closing something quietly after the real work has succeeded and a second failure changes nothing for the user. In those cases, name the variable ignored and write a comment saying why, so the next reader sees it was deliberate:

java
} catch (IOException ignored) {
    // The data was already saved; a failure while closing the preview changes nothing.
}

If you cannot write a convincing comment, the catch is not intentional, and it should not be empty. (Naming it ignored is a widely used convention, not a language rule.)

Examples

Example 1: spot it from the symptom (simple)

A teammate says: "The greeting is blank, but there is no error." You apply the hunt: reproduce (run it, see Welcome, !), narrow down (the name comes from loadName), search (the only catch in that method has an empty body), confirm (add a print inside it, run again, and it runs). Total time: a few minutes, because you knew what to look for. Without the habit, people often read the greeting code for an hour.

Example 2: why printStackTrace is not enough (practical)

Change the empty block to e.printStackTrace(); and run (the V1 output above). You now see the cause, NoSuchFileException: user.txt, which is progress for you as a developer. But the user still sees Welcome, !, the method still returns the wrong value, and in a production service the trace is only a line in a log that must be found. The decision about what the program should do is still missing. That decision, recover, report, or rethrow, is the fix.

Example 3: the fix, verified (realistic)

Apply the proper fix (V2 above) and re-run the same failing scenario. Output: Cannot start: Could not load the user name from user.txt and Reason: java.nio.file.NoSuchFileException: user.txt. The problem is now reported, with the file name and the root cause, and the program does not carry on with an invented empty name. Then run the success scenario too (Welcome, Sara!) to make sure the fix did not break the normal path.

A last improvement for the user is a friendlier message, for example "Create a file called user.txt next to the program", so the person knows what to do. In the lab, messages should be "helpful to the user", which means: what happened, with which value, and what to try.

Backend Insight

In a service, a swallowed exception can mean a payment that was never recorded, an email that was never sent, or a record that silently failed to save, while the request still returns "success". The logs contain nothing, because the failure was hidden before it reached the logging code. This is why many teams treat an empty catch as a code-review rejection, and why static analysis tools flag it.

The three-way choice (recover, report, rethrow) is how real error handling works in layered code. Low layers rethrow with a cause, and the top layer decides what the user is told and what goes into the log. Keeping the cause chain is what lets an engineer trace back from a vague top-level error to the real file, query, or value that failed.

Common Mistakes

Mistake 1: The empty catch

  • Incorrect: catch (Exception e) {}
  • Correct: catch the narrow type and recover, report, or rethrow.

Mistake 2: Returning a made-up default and carrying on

Returning "", 0, or null from a catch hides the failure by turning it into plausible data. Only do it when the default is truly correct and the user is told.

Mistake 3: Print and forget

e.printStackTrace() alone, or System.out.println(e) alone, helps the developer but leaves the program in the same wrong state. Add a decision.

Mistake 4: Rethrowing without the cause

throw new IllegalStateException("load failed") inside the catch throws away the original. Always pass e as the cause.

Mistake 5: Catching Throwable or Exception to be safe

It sweeps in bugs and errors that need to be seen. Catch the type you can handle.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. What is a "swallowed exception"?

Show solution A1

A1. An exception that was caught and then ignored, so the program carries on without reporting or handling the problem, often leaving the user with a wrong or empty result.

A2. Name two symptoms in a running program that suggest one.

Show solution A2

A2. Wrong or empty results with no error message; data that quietly goes missing; logs that look clean although something plainly failed.

A3. Is e.printStackTrace() inside an otherwise empty catch a complete fix? Answer in one sentence.

Show solution A3

A3. No: it shows the developer the cause, but the program still carries on with a wrong value and nobody is told what to do.

Level B: Application

B1. Fix this method so the failure is no longer hidden. The method should keep returning an int.

java
static int readAge(String text) {
    try {
        return Integer.parseInt(text);
    } catch (Exception e) {
    }
    return 0;
}
Show solution B1

B1.

java
static int readAge(String text) {
    try {
        return Integer.parseInt(text.trim());
    } catch (NumberFormatException e) {
        throw new IllegalArgumentException("Age must be a whole number, got '" + text + "'", e);
    }
}

The narrow type is used, the message names the value, and the cause is kept. Callers decide what to show.

B2. Give the three choices a good catch block can make, and say which you would pick for readAge if readAge is called from code that talks to the user.

Show solution B2

B2. Recover (use a default and tell the user), report (show or log a clear message), rethrow (with the cause). For readAge called from user-facing code, report and ask again: show "Age must be a whole number" and re-prompt, as in lesson 3. If the method is deeper in the code and cannot talk to the user, rethrow, as in B1.

B3. The following catch is intentionally empty. Rewrite it so a reviewer can see that it is deliberate.

java
try {
    preview.close();
} catch (IOException e) {
}
Show solution B3

B3.

java
try {
    preview.close();
} catch (IOException ignored) {
    // The data is already saved; a failure closing the preview does not matter.
}

Level C: Challenge

C1. Write a small program that reads the first line of settings.txt and uses it as a greeting. Write it first with a swallowing catch, run it with no file, then fix it by catching IOException, reporting a clear message, and keeping the cause. Paste both outputs.

Show solution C1

C1. Sketch: the first version has catch (Exception e) {} and returns "", so the run prints only the greeting prefix. The fixed version catches IOException, throws new IllegalStateException("Could not read settings.txt", e), and main prints Cannot start: Could not read settings.txt and the cause. Your output will match the pattern of V0 and V2 in this lesson. Run it and paste your own results.

C2. Review: fix try { load(); } catch (Exception e) {} where load() declares throws IOException, once by reporting and once by rethrowing. Explain which you would use in a method three levels below main.

Show solution C2

C2. Report: catch (IOException e) { System.out.println("Could not load data: " + e.getMessage()); }. Rethrow: catch (IOException e) { throw new IllegalStateException("Could not load data", e); }. Three levels below main, rethrow: a deep method cannot decide what the user should see, so it passes a clearer exception up with the cause and lets the top layer report it.

Mini Challenge

Write a swallowed-exception test for a classmate. Create a program of about 25 lines that parses a few numbers from text and a file name, with at least two catch blocks, one of which swallows an exception. Give it to someone else, or put it aside for a day, and then hunt it down using the five steps. Write down: the symptom, the line you suspected, how you confirmed it, the fix (recover, report, or rethrow), and the output before and after.

Check your work: your fix uses a narrow type, shows a message with the relevant value, keeps the cause if it rethrows, and the "after" output reports the problem.

Summary & Next Lesson

Key takeaways

  • A swallowed exception shows up as missing output or wrong results, with no error.
  • Hunt it: reproduce, narrow down, search for suspicious catch blocks, confirm, fix, and re-run the same scenario.
  • A good catch recovers, reports, or rethrows, on purpose. Printing the trace alone is not a decision.
  • Catch the narrowest type and keep the cause when rethrowing.
  • If ignoring is truly right, name the variable ignored and explain why in a comment.

Next lesson

This is the last lesson of the module. Next is the lab, "Bank with rules" (BEG-029-LAB1), where you build an Account that throws InsufficientFundsException, a console loop that retries on NumberFormatException, and a file read with try-with-resources. Its acceptance rule is the one you just practiced: no empty catch blocks.

Self-check

  1. What does a swallowed exception look like from the outside?
  2. Why is catch (Exception e) {} worse than a narrow catch with a message?
  3. Name the three choices for a good catch block.
  4. Why is it important to re-run the same failing scenario after the fix?
  5. How do you mark a deliberately ignored exception?
Show answers
  1. Wrong or missing results with no error message or trace.
  2. It hides every kind of failure, including bugs, and gives nobody any information.
  3. Recover, report, or rethrow (with the cause).
  4. To confirm the problem is now reported and the fix did not just move it, and to check the normal path still works.
  5. Name the variable ignored and add a comment explaining why ignoring is safe.

References

  • Oracle, The Java Tutorials, Catching and Handling Exceptions. Fetched. Supports the catch-block structure and handling by exception type.
  • Oracle, Java SE 21 API, Throwable. Fetched. Supports getCause() and the cause chain used when rethrowing.
  • The three program versions and their outputs were run with JDK 21.0.10 on Windows; the internal JDK trace lines differ by system. The ignored naming convention and the advice that teams reject empty catch blocks are common practice and were not checked against a fetched page. The solutions to B1, B3, and C2 were not run.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Type to search levels, phases, modules and lessons.