Exception Handling in Java: Best Practices and Common Mistakes - Quiz

Total: 5 questions

1. 

What is exception swallowing in Java and why is it dangerous?

Exception swallowing means catching an exception without reacting to it: an empty catch (often with a // TODO comment), a catch with only e.printStackTrace(), or replacing the error with null or -1. Such code compiles without warnings, but the program keeps running in a wrong state, and the real cause of the failure surfaces later and somewhere else.

  • Empty catch: the error turns into silence — for example, a loop that saves orders reports «no errors» although one of three orders was never saved.
  • Only printStackTrace(): execution continues (the customer gets a confirmation for an order that was not saved), and the output goes to System.err, bypassing the logger — no timestamp, level or context, and on a server it is often lost.
  • return null or -1: «no data» and «storage unavailable» become indistinguishable; the caller gets a NullPointerException later, and -1 can be a valid value. Express a missing result with Optional and a failure with an exception.

The fix: do not catch an exception where there is nothing to do with it; if the failure of one item must be tolerated, record it (collect it into a report or log it together with the exception object). If ignoring is intentional, name the parameter ignored and explain why in a comment; in Java 22+ you can write catch (NumberFormatException _).

2. 

Why should you not catch Exception or Throwable, and where in a program is catch (Exception e) acceptable?

A broad catch catches not only the error you expected, but everything else too:

  • catch (Exception e) catches every RuntimeException — bugs such as NullPointerException, ClassCastException, IndexOutOfBoundsException. With such a block, parseOrDefault(null, 0) returns the default value instead of failing, and a programmer mistake is disguised as «a string in the wrong format». It also catches InterruptedException, so the thread loses its interrupt flag.
  • catch (Throwable t) adds OutOfMemoryError and StackOverflowError; after them the state of the program is not guaranteed, and carrying on is dangerous.

The rule: catch the most specific type you know how to handle (NumberFormatException instead of Exception), and list several expected types with multi-catch.

catch (Exception e) is fine at the application boundary — the topmost place where one failed operation must not stop the others: the main method, an HTTP request handler, the loop of a worker thread processing a task queue. There the exception is logged with its stack trace, and the program moves on to the next task or returns a 500 error to the client. In Spring such a boundary is usually a single @ControllerAdvice. Catching Throwable is acceptable only at the top level of a thread or framework to log the error, after which an Error is rethrown.

3. 

What is the log-and-rethrow anti-pattern, and how should you log exceptions correctly?

Log-and-rethrow is when every layer (repository, service, controller) writes the exception to the log and rethrows it. One error produces several ERROR/SEVERE entries and several identical stack traces: monitoring counts several incidents, and it is hard to tell from the log where the error first occurred.

How to do it right:

  • Log once — where the exception is finally handled (usually the application boundary). If a catch block contains throw, the method does not handle the error, and a log call before it is almost always redundant.
  • An intermediate layer either leaves the exception alone or wraps it in an exception of its own layer, with context and the cause: throw new OrderStorageException("Could not save order id=" + id, e). Without the second argument the Caused by chain is broken.
  • Pass the exception object to the logger, not e.getMessage(): log.log(Level.SEVERE, "Could not save order id=" + orderId, e) in java.util.logging or log.error("... id={}", orderId, e) in SLF4J. The message has no stack trace or causes, and for new IllegalStateException() it is even null.
  • Write context: what you tried to do and with which data, without passwords, card numbers or tokens. Do not use printStackTrace() or System.out instead of a logger.
4. 

Checked or unchecked exception for your own API: how do you choose, and why do Spring and other frameworks choose unchecked?

The criterion is whether the calling code can do something sensible other than abort the operation. Checked exceptions suit expected external situations the caller can react to: a file is not found, a payment is declined by the bank, a service does not respond in time. Unchecked exceptions suit programmer mistakes and contract violations: a null or invalid argument, a call at the wrong time. A practical test: if almost every caller writes catch (XException e) { throw new RuntimeException(e); }, the exception should have been unchecked.

Why frameworks choose unchecked:

  • Checked exceptions scale poorly across layers: an SQLException from a repository requires throws in the service and the controller, JDBC details leak through the whole application, and the forced catch blocks often turn into swallowing. That is why Spring translates SQLException into the unchecked DataAccessException hierarchy, and JPA and Hibernate throw the unchecked PersistenceException.
  • Lambdas and the Stream API: Function, Consumer, ToIntFunction declare no throws, so a checked exception has to be wrapped inside every lambda (for IOException there is UncheckedIOException).
  • @Transactional by default rolls back only on RuntimeException and Error; on a checked exception the data is committed unless you specify rollbackFor = Exception.class — a common trick interview question.

Kotlin, Scala and C# have no checked exceptions at all.

5. 

How should you handle InterruptedException, and why must its catch block never be left empty?

InterruptedException is a checked exception meaning that another thread asked the current one to stop (called interrupt()). When Thread.sleep(), wait() or BlockingQueue.take() throw it, they clear the interrupt flag. If the flag is set before the call, sleep() throws the exception immediately.

So an empty catch erases the request to stop without a trace: a while (!Thread.currentThread().isInterrupted()) loop in a worker keeps spinning, and ExecutorService.shutdownNow(), which stops tasks precisely through interrupt(), cannot shut the pool down.

Correct options:

  • propagate the exception with throws InterruptedException;
  • if you cannot propagate it (for example, in Runnable.run()), restore the flag: catch (InterruptedException e) { Thread.currentThread().interrupt(); } — after that isInterrupted() returns true again.

A catch (Exception e) around code with sleep() or take() has the same effect as an empty catch: it needs a separate catch (InterruptedException e) before it that restores the flag.

Page 1 of 1