AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

12 Exceptions / Lesson 5 of 6

Custom exceptions and exception chaining

12 min · +10 XP · 2 exercises · 5 questions

Read the text12 min
Exercises0/2
Quiz5 q

Quick answer

How do you create a custom exception in Java?

To create a custom exception in Java, extend RuntimeException for an unchecked exception or Exception for a checked one, and pass a message — plus the cause, when wrapping — to the superclass constructor. Name it after the condition, such as InsufficientFundsException, build the message from the facts, and keep data a handler needs in final fields with accessors.

The JDK's exceptions describe mechanical failures — a null, an index, a missing file. Your program has domain failures: an insufficient balance, an unknown customer, an order that cannot be cancelled after shipping. Custom exception classes let callers catch those by name, carry the data needed to handle or report them, and hide the mechanical causes underneath. This lesson is how to design them, how to chain causes, and how to decide when one is warranted.

Declaring one

public class InsufficientFundsException extends RuntimeException {
    private final long requestedCents;
    private final long availableCents;

    public InsufficientFundsException(long requestedCents, long availableCents) {
        super("requested " + requestedCents + " but only " + availableCents + " available");
        this.requestedCents = requestedCents;
        this.availableCents = availableCents;
    }

    public long requestedCents() { return requestedCents; }
    public long availableCents() { return availableCents; }
}

Choices made here:

  • Superclass: RuntimeException for unchecked, Exception for checked (previous lesson's judgement). Never Throwable or Error.
  • Name ends in Exception and states the condition, not the location.
  • Message built in the constructor from the facts, so every thrower produces the same, informative text.
  • Fields for the data a handler might use — amounts, ids, the offending value — with accessors. Exceptions are objects; use that.
  • Fields final; the class need not be, but usually should be (subclassing exceptions for a hierarchy is fine; subclassing to alter behaviour is odd).

Provide the standard constructors when the exception is general-purpose: (String message), (String message, Throwable cause), and if useful (Throwable cause). A domain exception with specific fields, as above, often needs only its own.

Chaining: keep the cause

public Order load(long id) {
    try {
        return repository.find(id);
    } catch (SQLException e) {
        throw new OrderLoadException("could not load order " + id, e);      // e becomes getCause()
    }
}

Wrapping translates a low-level exception into one meaningful at this layer — callers of load should not know about SQL — while the original travels along as the cause. The trace then shows both:

OrderLoadException: could not load order 42
    at OrderService.load(OrderService.java:31)
    …
Caused by: java.sql.SQLException: connection reset
    at …

Rules: always pass the cause when wrapping (new X(msg, e), or initCause(e) for constructors that lack the parameter); never wrap just to change the type without adding context; never catch, log and rethrow a different exception without the cause — that is how "connection reset" becomes an unexplained "load failed" in production logs.

Exception hierarchies

public class BankException extends RuntimeException { … }
public class InsufficientFundsException extends BankException { … }
public class AccountFrozenException extends BankException { … }

A small hierarchy lets callers choose their granularity: catch (InsufficientFundsException e) for the specific case, catch (BankException e) for "anything the bank module signals". Keep it shallow — one base per module or layer, a handful of leaves. A hierarchy that mirrors every method is noise.

Translating across layers

Each layer of an application should throw exceptions in its own vocabulary: persistence throws data-access exceptions, services throw domain exceptions, the web layer turns them into HTTP responses. The translation points are the catch-and-wrap blocks at layer boundaries, and the causes preserve the full story for the logs. Letting SQLException reach a controller — or an HttpStatus reach a repository — is a design leak.

When a custom exception is warranted

Create one when at least one of these holds:

  • Callers will catch it specifically and do something different from what they do for other failures.
  • It carries structured data a handler needs (an id, an amount, a set of validation errors).
  • It marks a layer boundary and hides implementation types.
  • It names a domain condition that appears in requirements ("insufficient funds", "duplicate username").

Otherwise use the JDK's: IllegalArgumentException for bad arguments, IllegalStateException for bad timing, UnsupportedOperationException for unimplemented operations, NoSuchElementException for lookups that fail. A custom InvalidArgumentException that is IllegalArgumentException with a new name helps nobody.

Messages

A message is for the person reading the log, not for the program: state what was attempted and the values involved ("order 42 for customer 7 not found"), never just "error" or "invalid". Do not include secrets or personal data. Do not depend on messages programmatically — that is what types and fields are for.

Serialization note

Exceptions are Serializable; if one is ever serialised (RMI, some frameworks), its fields must be serialisable too and a serialVersionUID avoids warnings. For ordinary applications, ignore this.

Interview angle

  • "How do you write a custom exception?" Extend RuntimeException (or Exception), constructor with message and cause, fields for relevant data.
  • "What is exception chaining?" Wrapping a caught exception as the cause of a new one so the original trace is preserved and the type fits the layer.
  • "When should you create one rather than use IllegalArgumentException?" When callers catch it specifically, it carries data, or it hides a layer's internals.
  • "Why is losing the cause bad?" The real failure and its stack trace disappear from the logs.

Key takeaways

  • Extend RuntimeException (usually) or Exception; name the condition; build the message from the facts; carry data in final fields.
  • Wrap lower-level exceptions at layer boundaries, always with the cause.
  • Shallow hierarchies with one base per module let callers pick their granularity.
  • Prefer JDK exceptions unless callers catch yours specifically or it carries data.

Common questions

What is exception chaining in Java?

Exception chaining wraps a caught exception as the cause of a new one: throw new OrderLoadException("could not load order " + id, e). The new type fits the current layer, getCause() keeps the original, and the stack trace prints it under Caused by:.

Should a custom exception extend Exception or RuntimeException?

Extend RuntimeException for most application exceptions, so callers are not forced to catch or declare them; extend Exception only for a checked, recoverable condition at an API boundary. Never extend Throwable or Error.

When should you create a custom exception instead of using a built-in one?

When callers will catch it specifically, when it carries structured data a handler needs, when it hides a lower layer's exceptions at a boundary, or when it names a domain condition such as insufficient funds. Otherwise use IllegalArgumentException, IllegalStateException or another JDK exception.

Why should you always pass the cause when wrapping an exception?

Without the cause, the original exception and its stack trace vanish from the logs — "connection reset" becomes an unexplained "load failed". Pass it to the constructor, new X(message, e), or call initCause(e) when the constructor has no cause parameter.

What constructors should a custom exception have?

A general-purpose exception should offer (String message) and (String message, Throwable cause), and (Throwable cause) if useful. A domain exception with specific fields, such as requested and available amounts, often needs only its own constructor that builds the message from them.

Mark as readSign in to keep your place.

How to Create a Custom Exception in Java: Exception Chaining

Create a custom exception in Java by extending RuntimeException or Exception, with fields for its data. Chaining the cause, and when one is warranted.

  • Course: Java study plan
  • Module: Exceptions
  • Kind: Lesson
  • Reading time: 12 min
  • Runtime: OpenJDK 18

How do you create a custom exception in Java?

To create a custom exception in Java, extend RuntimeException for an unchecked exception or Exception for a checked one, and pass a message — plus the cause, when wrapping — to the superclass constructor. Name it after the condition, such as InsufficientFundsException, build the message from the facts, and keep data a handler needs in final fields with accessors.

Lesson

The JDK's exceptions describe mechanical failures — a null, an index, a missing file. Your program has domain failures: an insufficient balance, an unknown customer, an order that cannot be cancelled after shipping. Custom exception classes let callers catch those by name, carry the data needed to handle or report them, and hide the mechanical causes underneath. This lesson is how to design them, how to chain causes, and how to decide when one is warranted.

Declaring one

public class InsufficientFundsException extends RuntimeException {
    private final long requestedCents;
    private final long availableCents;

    public InsufficientFundsException(long requestedCents, long availableCents) {
        super("requested " + requestedCents + " but only " + availableCents + " available");
        this.requestedCents = requestedCents;
        this.availableCents = availableCents;
    }

    public long requestedCents() { return requestedCents; }
    public long availableCents() { return availableCents; }
}

Choices made here:

  • Superclass: RuntimeException for unchecked, Exception for checked (previous lesson's judgement). Never Throwable or Error.
  • Name ends in Exception and states the condition, not the location.
  • Message built in the constructor from the facts, so every thrower produces the same, informative text.
  • Fields for the data a handler might use — amounts, ids, the offending value — with accessors. Exceptions are objects; use that.
  • Fields final; the class need not be, but usually should be (subclassing exceptions for a hierarchy is fine; subclassing to alter behaviour is odd).

Provide the standard constructors when the exception is general-purpose: (String message), (String message, Throwable cause), and if useful (Throwable cause). A domain exception with specific fields, as above, often needs only its own.

Chaining: keep the cause

public Order load(long id) {
    try {
        return repository.find(id);
    } catch (SQLException e) {
        throw new OrderLoadException("could not load order " + id, e);      // e becomes getCause()
    }
}

Wrapping translates a low-level exception into one meaningful at this layer — callers of load should not know about SQL — while the original travels along as the cause. The trace then shows both:

OrderLoadException: could not load order 42
    at OrderService.load(OrderService.java:31)
    …
Caused by: java.sql.SQLException: connection reset
    at …

Rules: always pass the cause when wrapping (new X(msg, e), or initCause(e) for constructors that lack the parameter); never wrap just to change the type without adding context; never catch, log and rethrow a different exception without the cause — that is how "connection reset" becomes an unexplained "load failed" in production logs.

Exception hierarchies

public class BankException extends RuntimeException { … }
public class InsufficientFundsException extends BankException { … }
public class AccountFrozenException extends BankException { … }

A small hierarchy lets callers choose their granularity: catch (InsufficientFundsException e) for the specific case, catch (BankException e) for "anything the bank module signals". Keep it shallow — one base per module or layer, a handful of leaves. A hierarchy that mirrors every method is noise.

Translating across layers

Each layer of an application should throw exceptions in its own vocabulary: persistence throws data-access exceptions, services throw domain exceptions, the web layer turns them into HTTP responses. The translation points are the catch-and-wrap blocks at layer boundaries, and the causes preserve the full story for the logs. Letting SQLException reach a controller — or an HttpStatus reach a repository — is a design leak.

When a custom exception is warranted

Create one when at least one of these holds:

  • Callers will catch it specifically and do something different from what they do for other failures.
  • It carries structured data a handler needs (an id, an amount, a set of validation errors).
  • It marks a layer boundary and hides implementation types.
  • It names a domain condition that appears in requirements ("insufficient funds", "duplicate username").

Otherwise use the JDK's: IllegalArgumentException for bad arguments, IllegalStateException for bad timing, UnsupportedOperationException for unimplemented operations, NoSuchElementException for lookups that fail. A custom InvalidArgumentException that is IllegalArgumentException with a new name helps nobody.

Messages

A message is for the person reading the log, not for the program: state what was attempted and the values involved ("order 42 for customer 7 not found"), never just "error" or "invalid". Do not include secrets or personal data. Do not depend on messages programmatically — that is what types and fields are for.

Serialization note

Exceptions are Serializable; if one is ever serialised (RMI, some frameworks), its fields must be serialisable too and a serialVersionUID avoids warnings. For ordinary applications, ignore this.

Interview angle

  • "How do you write a custom exception?" Extend RuntimeException (or Exception), constructor with message and cause, fields for relevant data.
  • "What is exception chaining?" Wrapping a caught exception as the cause of a new one so the original trace is preserved and the type fits the layer.
  • "When should you create one rather than use IllegalArgumentException?" When callers catch it specifically, it carries data, or it hides a layer's internals.
  • "Why is losing the cause bad?" The real failure and its stack trace disappear from the logs.

Key takeaways

  • Extend RuntimeException (usually) or Exception; name the condition; build the message from the facts; carry data in final fields.
  • Wrap lower-level exceptions at layer boundaries, always with the cause.
  • Shallow hierarchies with one base per module let callers pick their granularity.
  • Prefer JDK exceptions unless callers catch yours specifically or it carries data.

Common questions

What is exception chaining in Java?

Exception chaining wraps a caught exception as the cause of a new one: throw new OrderLoadException("could not load order " + id, e). The new type fits the current layer, getCause() keeps the original, and the stack trace prints it under Caused by:.

Should a custom exception extend Exception or RuntimeException?

Extend RuntimeException for most application exceptions, so callers are not forced to catch or declare them; extend Exception only for a checked, recoverable condition at an API boundary. Never extend Throwable or Error.

When should you create a custom exception instead of using a built-in one?

When callers will catch it specifically, when it carries structured data a handler needs, when it hides a lower layer's exceptions at a boundary, or when it names a domain condition such as insufficient funds. Otherwise use IllegalArgumentException, IllegalStateException or another JDK exception.

Why should you always pass the cause when wrapping an exception?

Without the cause, the original exception and its stack trace vanish from the logs — "connection reset" becomes an unexplained "load failed". Pass it to the constructor, new X(message, e), or call initCause(e) when the constructor has no cause parameter.

What constructors should a custom exception have?

A general-purpose exception should offer (String message) and (String message, Throwable cause), and (Throwable cause) if useful. A domain exception with specific fields, such as requested and available amounts, often needs only its own constructor that builds the message from them.

Exercises

Validation with data

Write class ValidationException extends RuntimeException carrying the field name and the rejected value, with the message <field>: <reason> (was '<value>'). Write static int parseAge(String s) that throws it for a non-integer ("not a number", wrapping the NumberFormatException as the cause) or an age outside 0–150 ("out of range").

Read an integer n and n tokens. For each, print ok <age> or the exception's message followed by cause=<cause simple class name or none>.

Example: 3 then 30 abc 200 →

ok 30
age: not a number (was 'abc') cause=NumberFormatException
age: out of range (was '200') cause=none

Insufficient funds, with data

Write class InsufficientFundsException extends Exception carrying long shortfall (a field with a getter) and the message short by <shortfall>. class Account has a balance and void withdraw(long amount) throws InsufficientFundsException. Read an opening balance and n withdrawals; for each print withdrew <amount>, balance <b> or refused: <message> (shortfall=<n>) using the getter, and finally refused=<count>.

Example: 100 then 3 then 30 90 20 →

withdrew 30, balance 70
refused: short by 20 (shortfall=20)
withdrew 20, balance 50
refused=1

In this module: Exceptions

← try-with-resources and AutoCloseable · Exception handling that survives production →