Designing Your Own Exceptions

OOP · lesson 38 of 43 · 3 min read

When a new exception type earns its keep, what to put on it, and why the cause must survive.

Open this lesson in the learning hub

Key points

  • Extend RuntimeException for programming errors and bad input; extend Exception only when a caller can recover.
  • Add a type per recoverable outcome, not per message. If callers cannot act differently, reuse an existing type.
  • Give it fields, not just text: an orderId on the exception beats parsing it back out of a message.
  • Always pass the cause with super(message, cause). A trace with the original failure missing is a lost afternoon.
  • catch clauses are tried in the order you wrote them, so a specific type must be listed above its supertype.
  • One small family under a shared base type lets a caller catch the family or one member, whichever it can handle.

Example

public class Main {

    // One base type for the family, so a caller can catch broadly or narrowly.
    static class PaymentException extends RuntimeException {
        private final String orderId;

        PaymentException(String orderId, String message, Throwable cause) {
            super(message, cause);                 // never drop the cause
            this.orderId = orderId;
        }

        String orderId() { return orderId; }
    }

    static final class NotEnoughFunds extends PaymentException {
        private final int shortfall;

        NotEnoughFunds(String orderId, int shortfall) {
            super(orderId, "short by " + shortfall + " cents", null);
            this.shortfall = shortfall;            // data, not just a message
        }

        int shortfall() { return shortfall; }
    }

    static void charge(String orderId, int balance, int price) {
        if (price > balance) throw new NotEnoughFunds(orderId, price - balance);
        System.out.println("charged " + price + " for " + orderId);
    }

    public static void main(String[] args) {
        charge("A-1", 5000, 4200);

        try {
            charge("A-2", 100, 4200);
        } catch (NotEnoughFunds e) {               // the specific type comes first
            System.out.println("top up " + e.shortfall() + " cents on " + e.orderId());
        } catch (PaymentException e) {
            System.out.println("payment failed on " + e.orderId());
        }

        try {
            throw new PaymentException("A-3", "gateway timed out", null);
        } catch (NotEnoughFunds e) {               // wrong subtype, skipped
            System.out.println("top up " + e.shortfall());
        } catch (PaymentException e) {             // this one matches
            System.out.println("payment failed on " + e.orderId() + ": " + e.getMessage());
        } catch (RuntimeException e) {             // never reached
            System.out.println("something else");
        }

        try {
            try {
                Integer.parseInt("not-a-number");
            } catch (NumberFormatException cause) {
                throw new PaymentException("A-4", "bad amount on the request", cause);
            }
        } catch (PaymentException e) {
            System.out.println(e.getMessage() + " <- " + e.getCause().getClass().getSimpleName());
        }
    }
}

A new exception type is for a caller who will act on it, and it must carry the cause.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OOP course, and every lesson in it is listed on the OOP contents page.