Command Palette

Search for a command to run...

PHASE 7Intermediate ~31 min· topic 5 of 8

Topic 7.5

Writing Your Own Exceptions

In one line

A custom exception is a class you write that extends Exception (checked) or RuntimeException (unchecked). It gives a failure a meaningful name, can carry extra data, and should keep the original exception as its cause.

Think of it like this

A hospital's alarm codes. 'Something is wrong' isn't helpful, so hospitals use specific codes: one means a fire, another means a patient's heart has stopped. Each code tells the right team exactly what to do, and the announcement includes details like the room number. A custom exception is your program's specific alarm code: a precise name, the details needed to act, and a note of what originally triggered it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Custom exception
An exception class you write yourself, by extending Exception or RuntimeException.
Cause
The original exception that led to this one, stored inside it and returned by getCause().
Exception chaining
Wrapping a caught exception as the cause of a new one, so the full story of the failure is kept.
Exception translation
Catching a low-level exception (like SQLException) at a layer boundary and throwing one that fits the higher level (like OrderStorageException).
Root cause
The very first exception in a chain: the last Caused by: in a stack trace. Usually the real problem.
`super(...)`
A call, as the first line of a constructor, that runs the parent class's constructor with the given arguments (Phase 5).
Error code
A short, stable identifier such as OUT_OF_STOCK stored in an exception, so code and support teams can recognise the problem without reading the message text.
`serialVersionUID`
A version number for a class's serialized form. Throwable is serializable, so exceptions sent between JVMs should declare one.

Step by step

01The smallest useful custom exception

Choose a name that describes the failure, choose checked or unchecked by picking the superclass, and pass the message to super.

That's all it takes to be throwable and catchable. Everything else (getMessage, stack trace, cause) is inherited from Throwable.

Main.javawhole filejava
class OrderNotFoundException extends RuntimeException {
    OrderNotFoundException(String orderId) {
        super("no order with id " + orderId);
    }
}

// somewhere in a service:
throw new OrderNotFoundException("A-1042");

02The full template

For a reusable exception, provide the standard constructors so callers can always attach a cause. Keep any fields final and add no setters.

serialVersionUID matters only if exceptions are serialized (RMI, some distributed caches, old remoting frameworks). Declaring it costs one line and avoids InvalidClassException when two sides compile different versions.

PaymentException.javawhole filejava
public class PaymentException extends Exception {          // checked
    private static final long serialVersionUID = 1L;

    public PaymentException(String message) {
        super(message);
    }

    public PaymentException(String message, Throwable cause) {
        super(message, cause);
    }

    public PaymentException(Throwable cause) {
        super(cause);                 // message becomes cause.toString()
    }
}

03Keep the cause: chaining

When you catch something low-level and throw something higher-level, always pass the caught exception along. Without it, the log says 'cannot load config' and nobody knows whether the file was missing, unreadable or malformed.

The printed trace shows the new exception first, then Caused by: with the original. ... 1 more means the remaining frames are identical to the frames above, so they're not repeated.

Main.javawhole filejava
try {
    text = readFile(file);
} catch (IOException e) {
    throw new ConfigException("cannot load config from " + file, e);   // e is the cause
}
terminal
$ java Main.java
── expected output ──
Exception in thread "main" Main$ConfigException: cannot load config from missing.properties
at Main.loadPort(Main.java:19)
at Main.main(Main.java:32)
Caused by: java.io.IOException: file not found: missing.properties
at Main.readFile(Main.java:12)
at Main.loadPort(Main.java:17)
... 1 more

04Carry data, not just text

If a handler would need to parse the message to get a number back out, put the number in a field instead. Messages are for humans; fields are for code.

Here a handler can show shortfall() to the user or decide whether to offer an overdraft, without string parsing.

Main.javawhole filejava
class InsufficientFundsException extends RuntimeException {
    private final long balance;
    private final long requested;

    InsufficientFundsException(long balance, long requested) {
        super("balance " + balance + " is less than requested " + requested);
        this.balance = balance;
        this.requested = requested;
    }

    long shortfall() { return requested - balance; }
}

05A small hierarchy for one domain

A base exception for a module lets callers catch 'anything that went wrong in the shop' in one place, while specific subclasses can be handled specially where it matters.

Keep it shallow: a base type and a handful of subtypes that callers actually distinguish. Deep trees of exception classes nobody catches separately are maintenance without benefit.

A small hierarchy for one domaindiagram
Rendering diagram…

06Translate at layer boundaries

A web controller shouldn't need to know that orders are stored in PostgreSQL. If the repository lets SQLException escape, every caller depends on that detail. Translating keeps each layer's exceptions in its own vocabulary, and chaining keeps the details for debugging.

This is the same idea as Spring's DataAccessException hierarchy, which translates vendor-specific SQL errors into consistent unchecked exceptions (see the System Design course for layered architecture).

Translate at layer boundariesdiagram
Rendering diagram…

07Rules the compiler enforces

A custom checked exception follows catch-or-declare like any other. A generic class can't extend Throwable (class Failure<T> extends Exception is an error), because the JVM's catch matching works on erased run-time classes, so catch (Failure<String> e) could never be checked.

Constructors aren't inherited: if you only write (String message), then new MyException("x", e) doesn't compile. Add the constructors your callers need.

terminal
$ javac Main.java
── expected output ──
Main.java:1: error: a generic class may not extend java.lang.Throwable
class Failure<T> extends Exception {
^
1 error

Try it yourself

  1. 1

    Add a field to the hierarchy

    Give OutOfStockException a field item with an accessor, and in the handler print try " + e.item() + " later. Predict the new output line, then run.

  2. 2

    Remove the cause

    In the chaining example, change new ConfigException("cannot load config from " + file, e) to drop e. Predict which caused by line disappears. Imagine reading only that output in a production log at 3 a.m.

  3. 3

    Make it checked

    Change ShopException to extend Exception instead of RuntimeException. Predict where the compiler now complains (hint: order and its throws). Fix it by adding throws ShopException and run again.

Code & diagrams

An unchecked exception that carries data New tab
Sign in to run this example in your browser.

Expected output

error: balance 200 is less than requested 450
you need 250 more
is RuntimeException? true
balance: 200
A checked exception with chaining and translation New tab

The for loop walks the cause chain down to the root cause, as a logging library does.

Sign in to run this example in your browser.

Expected output

cannot load config from missing.properties
  caused by IOException: file not found: missing.properties
bad port '80x' in app.properties
  caused by NumberFormatException: For input string: "80x"
A small exception hierarchy with error codes New tab
Sign in to run this example in your browser.

Expected output

ordered tea
suggest something else: mango is out of stock
[PAYMENT_DECLINED] payment declined: card blocked
Losing the cause vs keeping it (fragment)java
// BAD: the IOException (and its message and trace) is gone forever
catch (IOException e) {
    throw new ConfigException("cannot load config");
}

// BAD: the message survives, the type and stack trace don't
catch (IOException e) {
    throw new ConfigException("cannot load config: " + e.getMessage());
}

// GOOD: full chain preserved, printed as "Caused by: java.io.IOException: ..."
catch (IOException e) {
    throw new ConfigException("cannot load config from " + file, e);
}

// For an exception class without a cause constructor (rare, old APIs):
ConfigException ce = new ConfigException("cannot load config");
ce.initCause(e);          // allowed once, only if no cause was set yet
throw ce;

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Throw a custom checked exception without declaring it

With class AppException extends Exception, write static void run() { throw new AppException("x"); }.

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: unreported exception AppException; must be caught or declared to be thrown
static void run() { throw new AppException("x"); }
^
1 error

Break #2

Make an exception generic

Declare class Failure<T> extends Exception { } to carry a typed payload.

terminal
$ javac Main.java
── what you'll see ──
Main.java:1: error: a generic class may not extend java.lang.Throwable
class Failure<T> extends Exception {
^
1 error

Myth vs fact

Myth

Every error deserves its own exception class.

Fact

Only when a caller will catch it specifically or it carries data. Otherwise a standard IllegalArgumentException or IllegalStateException with a good message is clearer and costs nothing to maintain.

Myth

Putting the cause's message in the new message is enough.

Fact

That keeps the text but loses the cause's type, its stack trace and any deeper causes. Pass the exception itself as the cause.

Myth

Custom exceptions should be checked so callers can't ignore them.

Fact

Choose by whether the caller can recover. Many teams make most domain exceptions unchecked and handle them at a boundary (a web layer's exception handler), keeping checked exceptions for truly recoverable cases.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Throwable.getMessage() returns the detail message passed to the constructor; getLocalizedMessage() defaults to it and can be overridden for translations; toString() is getClass().getName() + ": " + getLocalizedMessage(). The Throwable(Throwable cause) constructor sets the message to cause.toString(), which is why wrapped exceptions show the inner class name in their message.

  • ▸

    initCause can be called only once and only if the cause wasn't set by a constructor (otherwise it throws an IllegalStateException whose message starts Can't overwrite cause). Passing the exception itself throws IllegalArgumentException: Self-causation not permitted. Cycles longer than one are possible and printStackTrace detects them, printing [CIRCULAR REFERENCE: ...].

  • ▸

    Domain exceptions that carry fields are often mapped to API responses in one place, for example a Spring @ControllerAdvice that turns OutOfStockException into HTTP 409 with {"code": "OUT_OF_STOCK"}. Stable codes, not messages, are what clients should depend on.

  • ▸

    For exceptions used purely for control flow inside a library (rare, but parsers do it), the protected Throwable(String, Throwable, boolean enableSuppression, boolean writableStackTrace) constructor (Java 7) makes a stackless, cheap exception. Never expose such exceptions to users: they're undebuggable by design.

Remember this

  1. 1

    Creating one is ordinary inheritance (Phase 5): class OrderNotFoundException extends RuntimeException { ... }. The superclass decides the category: extend Exception (or another checked type such as IOException) for a checked exception, or RuntimeException (or a subclass such as IllegalStateException) for an unchecked one. Never extend Throwable or Error directly; Error means the JVM is failing.

  2. 2

    Give it the constructors callers need, passing values up with super(...). Exception and RuntimeException offer four public constructors: (), (String message), (String message, Throwable cause) and (Throwable cause). Constructors aren't inherited, so your class has only the ones you write. At minimum, provide (String message) and (String message, Throwable cause).

  3. 3

    Exception chaining (Java 1.4) is the key habit. When you catch a low-level exception and throw a higher-level one, pass the original as the cause: throw new ConfigException("cannot load " + file, e);. The cause travels with the new exception (getCause()), and the stack trace prints a Caused by: section, so nobody loses the real reason. Dropping the cause is one of the most common and costly mistakes in Java code.

  4. 4

    Custom exceptions can carry fields: an error code, the order ID, the balance and the amount requested. Handlers can then make decisions from data rather than parsing message strings. Make the fields final and set them in the constructor; an exception is a report of something that already happened, so it shouldn't change.

  5. 5

    A small hierarchy works well for a domain: a base ShopException and subclasses OutOfStockException, PaymentDeclinedException. Callers can catch one specific case, or the base type for everything from your module. Combined with chaining, this is exception translation: a repository catches SQLException and throws OrderStorageException, so callers don't depend on the fact that you use SQL.

  6. 6

    Create one only when it adds something: a caller will catch it specifically, or it carries data. For a plain bad argument, IllegalArgumentException already says everything. Name custom exceptions with the Exception suffix, write a message that includes the relevant values, and remember that Throwable implements Serializable (declare a serialVersionUID if your exceptions cross process boundaries). A generic class can't extend Throwable at all, because catch can't distinguish type arguments that are erased at run time.

Explain it without notes

01

How do you decide whether a custom exception should extend Exception or RuntimeException?

02

What is exception chaining, and why is it important?

03

What constructors should a custom exception provide, and why?

04

When is a custom exception worth creating, and when should you use a standard one?

05

What is exception translation, and where do you do it?

Practice

01

Write an unchecked InvalidPinException with a field attemptsLeft. A method checkPin(String pin) accepts "1234" and otherwise throws it with the attempts left (start at 3). Try "0000", "1111" and "1234", printing what happens.

02

Write a checked ParseException-style class CsvException with a line field. A method int sumColumn(String[] lines) parses the second comma-separated value of each line and wraps any NumberFormatException with the line number (starting at 1) as the cause. Run it on {"a,1", "b,2", "c,x"}.

03

Write a method Throwable rootCause(Throwable t) that follows getCause() to the end. Test it on a three-level chain and print the root's class and message.

Trade-offs

  • ↔

    Specific exception classes make handling precise and self-documenting, but every class is API surface to maintain; a base exception with an error-code field can replace a dozen tiny subclasses.

  • ↔

    Translating exceptions at each layer decouples callers from implementation details, but over-wrapping produces chains five levels deep that are hard to read; translate at real boundaries, not in every method.

  • ↔

    Rich fields help handlers decide, but exceptions are often logged or serialized; avoid putting large objects or sensitive data (passwords, card numbers) into exception fields and messages.

Done when you can

  • Done when you can write a checked and an unchecked custom exception with the standard constructors.

  • Done when you always pass the caught exception as the cause when wrapping.

  • Done when you can read a trace with several Caused by: sections and find the root cause.

  • Done when you can design a small exception hierarchy with a base type and an error code.

  • Done when you can decide between a custom exception and a standard one, and justify it.