skip to content

When is a custom exception class actually worth creating versus reusing a standard JDK exception, and what design pitfalls should you avoid?

level: seniorimportance: should knowfreq 48%

answer

  1. Reuse JDK (IllegalArgument/IllegalState/UnsupportedOperation) when it fits
  2. Custom when: distinct catchability, domain meaning, or structured context
  3. Shallow shared base (DomainException) for uniform handling
  4. Don't use exceptions for normal control flow
  5. Avoid type explosion and leaky checked exceptions

basics

~20 s

Create a custom exception when callers need to catch that specific condition or when it carries domain meaning/extra context. Otherwise reuse a standard JDK exception like IllegalArgumentException. Don't create a new class for every error.

solid answer

~50 s

Reach for an existing JDK exception when one fits cleanly: IllegalArgumentException for bad arguments, IllegalStateException for wrong-state calls, UnsupportedOperationException, NullPointerException for null contracts. They're well understood and require no new types. Create a custom exception when (1) callers need to catch and handle that exact condition distinctly, (2) it represents a meaningful domain concept (OrderNotFoundException), or (3) it should carry structured context such as an error code or entity id. Good practice: name it with an Exception suffix; provide the four Throwable-mirroring constructors; preserve causes when wrapping; choose checked vs unchecked by recoverability; and often base your domain exceptions on a small shared parent so cross-cutting handling (logging, HTTP mapping) is uniform. Pitfalls: exploding the type count, swallowing causes, using exceptions for ordinary control flow, putting secrets in messages, and making checked exceptions that leak lower layers upward.

go deeper

for a junior

Knows custom exceptions exist and that some standard ones (IllegalArgumentException) can be reused.

for a middle

Can choose between a standard JDK exception and a custom one for common cases and follows naming/constructor conventions.

for a senior

Articulates the catchability/domain/context criteria, designs a shallow base hierarchy, preserves causes, and avoids control-flow misuse and leaky checked exceptions.

for a principal

Sets organization-wide exception taxonomy and translation/mapping strategy (error codes, layer boundaries, observability), balancing reuse vs. domain expressiveness and preventing type sprawl.

## The core question: new type or reuse? Exceptions are part of your API. A new exception **type** is justified only when it earns its keep. The deciding factor is usually **how callers will react**: callers distinguish failures by their *type* in `catch` clauses, so a distinct type is worthwhile when a caller would plausibly want to catch *this* condition separately from others. ## When to reuse a standard JDK exception The JDK ships precise, widely-understood unchecked exceptions; prefer them when they fit: - **`IllegalArgumentException`** — a parameter's value is invalid (e.g. negative count). - **`IllegalStateException`** — the object is in the wrong state for the call (e.g. `next()` after iterator exhausted). - **`NullPointerException`** — a parameter that must be non-null was null (idiomatic via `Objects.requireNonNull`). - **`UnsupportedOperationException`** — an operation isn't supported by this implementation. - **`IndexOutOfBoundsException`**, **`NumberFormatException`**, etc. for their specific cases. Reusing these means zero new types, instant familiarity, and consistency with the rest of the platform. ## When a custom exception is worth it Create one when at least one of these holds: 1. **Distinct catchability** — a caller should be able to `catch (OrderNotFoundException e)` and react differently than to other failures (e.g. return HTTP 404). If no caller will ever single it out, a custom type adds little. 2. **Domain meaning** — the failure is a first-class concept in your domain (`InsufficientFundsException`, `OrderNotFoundException`). The type itself documents the API. 3. **Structured context** — you need to attach machine-readable data (an `errorCode`, the offending `entityId`, retry-ability) beyond a free-text message. Add fields + accessors. ## Design good practices - **Naming:** suffix with `Exception`; name the condition, not the throwing site. - **Constructors:** provide the four `Throwable`-mirroring forms (no-arg, message, message+cause, cause) so wrapping/chaining works everywhere. - **Preserve causes:** when translating a lower-level exception, always pass it as the cause. - **Checked vs unchecked:** decide by recoverability; default unchecked unless forcing the caller to handle adds real value. - **Base class hygiene:** a small shared parent (e.g. `DomainException extends RuntimeException` holding an `errorCode`) lets you handle a whole family uniformly (one `@ExceptionHandler`, consistent logging). Keep the hierarchy shallow. - **Immutability:** make context fields `final`. - **Serialization:** if exceptions cross process/serialization boundaries, mind `serialVersionUID` and that fields are serializable. ## Pitfalls to avoid - **Type explosion:** one exception class per error message bloats the codebase and helps no one. Group by how callers handle them. - **Swallowing the cause:** wrapping without passing the original loses the root cause. - **Exceptions for control flow:** throwing/catching to implement normal branching is slow (stack-trace capture) and obscures intent; use return values/`Optional` for expected outcomes. - **Leaky checked exceptions:** a checked custom exception declared up through many layers couples callers to lower internals; this is a prime reason teams favor unchecked. - **Sensitive data in messages:** messages end up in logs; never include passwords, tokens, or PII. - **Overriding `fillInStackTrace` carelessly:** sometimes done for performance (control-flow exceptions), but it removes the stack trace and harms debuggability — do it only deliberately. ## Decision summary > Reuse a JDK exception when it fits and no caller needs to single out the condition. Create a custom one when callers must catch it distinctly, it carries domain meaning, or it needs structured context — then give it conventional constructors, preserve causes, pick checked/unchecked by recoverability, and root it in a small shared base for uniform handling.

  • Why is using exceptions for normal control flow discouraged?
    Exceptions are for exceptional conditions. They capture a stack trace (costly) and make normal branches hard to follow; expected outcomes are better modeled with return values, Optional, or result types.
  • What's the benefit of a shared base exception class?
    A shallow common parent (e.g. DomainException with an errorCode) lets you catch and handle a whole family uniformly — one exception handler, consistent logging and HTTP mapping — without enumerating every subtype.

saying these in an interview costs you the question

  • Creating a new exception class for every individual error message
  • Using exceptions to implement ordinary control flow (expected branches)
  • Custom checked exceptions that leak lower-layer details up the stack
  • Inventing a custom type when IllegalArgumentException already says it
  • Putting sensitive data in exception messages that get logged

context