Skip to content
  • Custom Exceptions & Best Practices · Java

Custom Exceptions & Best Practices

LearnViaNotes @learnvianotes· 3 min read

Define your own exception types when callers need to react to a specific failure — and always keep the original cause attached.

Defining one

Java
public class Main {
    public static void main(String[] args) {
        Account acc = new Account(100);
        try {
            acc.withdraw(30);
            acc.withdraw(500);
        } catch (InsufficientFundsException e) {
            System.out.println(e.getMessage());
            System.out.println("short by " + e.shortfall());
        }
    }
}

// Checked: the caller can reasonably recover (show a message, offer a smaller amount)
class InsufficientFundsException extends Exception {
    private final long shortfall;
    InsufficientFundsException(long balance, long requested) {
        super("balance " + balance + " < requested " + requested);
        this.shortfall = requested - balance;
    }
    long shortfall() { return shortfall; }
}

class Account {
    private long balance;
    Account(long balance) { this.balance = balance; }
    void withdraw(long amount) throws InsufficientFundsException {
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive");  // bug → unchecked
        if (amount > balance) throw new InsufficientFundsException(balance, amount);
        balance -= amount;
    }
}
Output
balance 70 < requested 500
short by 430

Checked or unchecked?

ChooseWhenExtend
CheckedThe caller can and should recover: file missing, payment declined, validation of external inputException
UncheckedProgramming errors / violated preconditions, or failures nobody nearby can handleRuntimeException

Modern code (Spring, most libraries, lambdas and streams) leans unchecked: checked exceptions don't compose with functional interfaces like Function, and they leak implementation details up the call chain.

Exception chaining: keep the cause

Java
public class Main {
    static class ConfigException extends RuntimeException {
        ConfigException(String msg, Throwable cause) { super(msg, cause); }
    }

    static int loadPort(String raw) {
        try {
            return Integer.parseInt(raw);
        } catch (NumberFormatException e) {
            throw new ConfigException("invalid port in config: " + raw, e);   // wrap, keep cause
        }
    }

    public static void main(String[] args) {
        try {
            loadPort("80a");
        } catch (ConfigException e) {
            System.out.println(e.getMessage());
            System.out.println("caused by " + e.getCause().getClass().getSimpleName()
                               + ": " + e.getCause().getMessage());
        }
    }
}
Output
invalid port in config: 80a
caused by NumberFormatException: For input string: "80a"

Translate low-level exceptions into ones that match your layer's abstraction (a repository throws DataAccessException, not SQLException) — but pass the original as the cause so the stack trace still shows the root problem (Caused by: …).

Anti-patterns

Anti-patternWhy it hurtsInstead
catch (Exception e) {}Swallows the failure — the bug disappears silentlyHandle it, or let it propagate
e.printStackTrace() and continueOutput goes to stderr, program runs on with bad stateLog with a logger and rethrow/handle
throw new RuntimeException(e.getMessage())Loses the original type and stack tracethrow new XException("context", e)
Catch-log-rethrow at every layerThe same error is logged five timesLog once, where it's handled
Exceptions for control flowSlow (stack trace capture) and hides intentUse if, Optional, return values
throws Exception on every methodCallers can't tell what can go wrongDeclare specific types
Catching NullPointerExceptionMasks a bugFix the null source / validate early

Best practices

  • Fail fast: validate arguments at the start of a method (Objects.requireNonNull, IllegalArgumentException).

  • Use standard exceptions when they fit: IllegalArgumentException, IllegalStateException, UnsupportedOperationException, NoSuchElementException.

  • Put useful context in the message: the bad value, the id, the expected range.

  • Name custom types …Exception and provide (String message) and (String message, Throwable cause) constructors.

  • Restore the interrupt flag if you catch InterruptedException and can't rethrow it: Thread.currentThread().interrupt();.

  • Clean up with try-with-resources, not finally blocks.

How do you create a custom exception?

What is exception chaining and why use it?

Why are exceptions expensive?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report