skip to content

What is the difference between exception translation, exception chaining, and exception masking (swallowing)?

level: middleimportance: should knowfreq 55%

answer

  1. Translation = type, chaining = cause, masking = loss
  2. Translate AND chain; never mask
  3. finally throwing hides the real exception
  4. try-with-resources → addSuppressed, nothing lost
  5. 'Caused by:' vs 'Suppressed:'

basics

~20 s

Translation = throw a new, higher-level exception. Chaining = attach the original as the cause of that new exception. Masking = throw or catch in a way that loses the original. You usually translate AND chain, and never mask.

solid answer

~50 s

These three terms are often confused. Exception translation is converting a low-level exception into a higher-level, layer-appropriate one — it's about the *type* you expose. Exception chaining is the mechanism of attaching the original exception as the cause of the new one, via new Wrapper(msg, cause) or initCause, so the 'Caused by:' trace survives. They're complementary: good translation always uses chaining. Exception masking (or swallowing) is the anti-pattern where the original is lost — either by catching and ignoring it, or by throwing a new exception without passing the cause, or by a finally/try-with-resources interaction that overwrites it. A classic masking bug is when a finally block throws, hiding the real exception from the try; Java's suppressed-exception mechanism (Throwable.addSuppressed, used by try-with-resources) exists to prevent exactly that. The rule: translate when it adds abstraction value, chain so nothing is lost, never mask.

go deeper

for a junior

Knows that ignoring an exception (empty catch) is bad and that you should keep the original somehow.

for a middle

Clearly distinguishes translation (type), chaining (cause), and masking (loss); always chains when rethrowing.

for a senior

Explains the finally-overwrites trap and how try-with-resources / addSuppressed solves it; knows suppressed vs caused-by.

for a principal

Sets team conventions and lint rules to forbid swallowing/masking, reasons about how cause/suppressed chains surface in logging and monitoring at scale.

## Three terms, three meanings These words get used interchangeably in interviews but mean distinct things. ### 1. Exception translation **What:** catching a low-level exception and throwing a *different, higher-level* exception that fits the current abstraction layer. It's about the **type** the caller sees. Example: catch `SQLException`, throw `DataAccessException`. ### 2. Exception chaining **What:** the **mechanism** of linking the original exception to the new one as its *cause*. In Java, every `Throwable` has an optional `cause`. You set it via: - the standard constructor: `new MyException("message", originalException)` - or `myException.initCause(originalException)` if no such constructor exists. The cause is printed in stack traces as a `Caused by:` block. Chaining is what makes translation safe — you change the type *without throwing away* the evidence. Translation and chaining almost always go together: you translate the type and chain the cause. ```java try { parse(input); } catch (NumberFormatException e) { throw new ConfigException("Invalid port in config", e); // translate (new type) + chain (e as cause) } ``` ### 3. Exception masking (a.k.a. swallowing / shadowing) **What:** an **anti-pattern** where the original exception's information is *lost*. Forms: - **Swallowing:** `catch (Exception e) { }` — caught and ignored entirely. The error vanishes. - **Translating without chaining:** `throw new ConfigException("bad config");` — a new exception is thrown but the original `e` is not passed as cause, so you lose the real reason. - **Overwriting in finally:** if the `try` block throws exception A and the `finally` block then throws exception B, the JVM propagates B and **A is lost**. The same can happen when closing a resource throws while the body already failed. ## The finally/close masking trap and 'suppressed' exceptions Consider: ```java try { throw new IOException("real problem"); } finally { throw new RuntimeException("cleanup failed"); // this WINS; IOException is lost } ``` The `IOException` — the real cause — is silently discarded. To address this, Java 7 added the **suppressed-exception** mechanism: `Throwable.addSuppressed(Throwable)` and `getSuppressed()`. **try-with-resources** uses it automatically: if the body throws and then `close()` also throws, the body's exception propagates as primary and the `close()` exception is attached as a *suppressed* exception (shown as `Suppressed:` in the trace) — nothing is lost. This is why try-with-resources is preferred over manual try/finally for closing resources. ## How they relate - **Translation + chaining = the correct pattern.** Change the type for abstraction cleanliness; keep the cause for diagnostics. - **Translation WITHOUT chaining = masking.** A surprisingly common bug — you intended to translate but destroyed the evidence. - **Chaining without translation** is possible (wrap in a generic `RuntimeException(e)`) but pointless if the wrapper adds no abstraction value; it just deepens the trace. ## Practical rules 1. Never swallow: an empty catch must be justified with a comment and usually logged. 2. Always pass the cause when rethrowing. 3. Use try-with-resources so cleanup failures become *suppressed*, not masking, exceptions. 4. Don't let `finally` throw; if cleanup can fail, handle it inside the finally.

  • What is a suppressed exception and when does it occur?
    A secondary exception attached via Throwable.addSuppressed, shown as 'Suppressed:' in the trace. try-with-resources adds the close() failure as suppressed when the body already threw, so the primary exception isn't masked.
  • Show a masking bug that translation introduces.
    throw new ConfigException("bad config") inside a catch(IOException e) without passing e — the IOException's real reason and stack are lost. Fix: throw new ConfigException("bad config", e).

saying these in an interview costs you the question

  • Saying translation and chaining are the same thing
  • Thinking an empty catch block is acceptable without justification
  • Not knowing that a throwing finally overwrites the real exception
  • Confusing suppressed exceptions with the cause chain

context