00:00

Custom Exception in Java – Create Your Own Exceptions

Last updated: 7 min read

Java's built-in exceptions describe general problems such as a null reference or a missing file. But your application has its own kinds of failure: an account without enough balance, a product that does not exist, an order that cannot ship. A custom exception (also called a user-defined exception) gives each of these problems a clear name and can carry extra information about what went wrong.

This tutorial shows how to create checked and unchecked custom exceptions, add useful data and constructors, build a small exception hierarchy, and follow the best practices for designing exceptions.

1. How to Create a Custom Exception

A custom exception is a class that extends Exception (checked) or RuntimeException (unchecked):

Minimal custom exceptions
// Checked: callers must catch or declare it
public class InvalidOrderException extends Exception {
    public InvalidOrderException(String message) { super(message); }
    public InvalidOrderException(String message, Throwable cause) { super(message, cause); }
}

// Unchecked: no throws clause needed
public class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(String message) { super(message); }
}

Calling super(message) stores the message so that getMessage() and stack traces show it. The constructor with a cause lets you wrap another exception.

2. A Checked Custom Exception with Extra Data

Custom exceptions can carry fields that help callers react intelligently, not just a message:

CheckedCustom.java
public class CheckedCustom {
    // A checked custom exception: callers MUST handle or declare it
    static class InsufficientBalanceException extends Exception {
        private final double shortfall;

        InsufficientBalanceException(double requested, double available) {
            super("Requested " + requested + " but only " + available + " is available");
            this.shortfall = requested - available;
        }

        double getShortfall() { return shortfall; }
    }

    static double balance = 500;

    static void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) throw new InsufficientBalanceException(amount, balance);
        balance -= amount;
        System.out.println("Withdrawn " + amount + ", remaining " + balance);
    }

    public static void main(String[] args) {
        try {
            withdraw(200);
            withdraw(450);
        } catch (InsufficientBalanceException e) {
            System.out.println("Failed: " + e.getMessage());
            System.out.println("You need " + e.getShortfall() + " more");
        }
    }
}
Output
Withdrawn 200.0, remaining 300.0
Failed: Requested 450.0 but only 300.0 is available
You need 150.0 more

Because it extends Exception, withdraw() must declare it, and the compiler makes every caller deal with it.

3. An Unchecked Custom Exception

UncheckedCustom.java
import java.util.Map;

public class UncheckedCustom {
    // An unchecked custom exception: no throws clause needed
    static class ProductNotFoundException extends RuntimeException {
        ProductNotFoundException(String sku) {
            super("No product with SKU " + sku);
        }
    }

    static final Map<String, Double> PRICES = Map.of("PEN-1", 20.0, "BAG-7", 899.0);

    static double priceOf(String sku) {
        Double price = PRICES.get(sku);
        if (price == null) throw new ProductNotFoundException(sku);
        return price;
    }

    public static void main(String[] args) {
        System.out.println("PEN-1 costs " + priceOf("PEN-1"));
        try {
            priceOf("CUP-3");
        } catch (ProductNotFoundException e) {
            System.out.println(e.getMessage());
        }
    }
}
Output
PEN-1 costs 20.0
No product with SKU CUP-3

No throws clause is needed, so methods stay simple, but callers are also not reminded to handle it. Document it with a Javadoc @throws tag.

4. Checked or Unchecked?

Choose checked (extends Exception) when…Choose unchecked (extends RuntimeException) when…
The caller can reasonably recover (retry, ask the user, use another option)The problem is a programming error or invalid input that should be fixed
You want the compiler to force callers to think about itMost callers cannot do anything useful about it
It is part of a library API contractYou use frameworks such as Spring that handle exceptions centrally

Many modern codebases prefer unchecked custom exceptions and handle them at application boundaries, but both approaches are valid.

5. Building an Exception Hierarchy

A base exception for an area of your application, with more specific subclasses, lets callers catch broadly or precisely as needed:

ExceptionHierarchyDemo.java
public class ExceptionHierarchyDemo {
    // A small hierarchy lets callers catch broadly or precisely
    static class OrderException extends RuntimeException {
        OrderException(String message) { super(message); }
        OrderException(String message, Throwable cause) { super(message, cause); }
    }
    static class OutOfStockException extends OrderException {
        OutOfStockException(String item) { super(item + " is out of stock"); }
    }
    static class PaymentDeclinedException extends OrderException {
        PaymentDeclinedException(String reason, Throwable cause) { super("Payment declined: " + reason, cause); }
    }

    static void placeOrder(String item, String card) {
        if (item.equals("lamp")) throw new OutOfStockException(item);
        try {
            if (card.length() != 16) throw new IllegalArgumentException("card number must have 16 digits");
        } catch (IllegalArgumentException e) {
            throw new PaymentDeclinedException("invalid card", e);    // wrap with cause
        }
        System.out.println("Order placed for " + item);
    }

    public static void main(String[] args) {
        String[][] orders = {{"chair", "1234567812345678"}, {"lamp", "1234567812345678"}, {"desk", "1234"}};
        for (String[] o : orders) {
            try {
                placeOrder(o[0], o[1]);
            } catch (OutOfStockException e) {            // precise handling
                System.out.println("Suggest alternatives: " + e.getMessage());
            } catch (OrderException e) {                 // everything else about orders
                System.out.println(e.getMessage() + " (cause: " + e.getCause().getMessage() + ")");
            }
        }
    }
}
Output
Order placed for chair
Suggest alternatives: lamp is out of stock
Payment declined: invalid card (cause: card number must have 16 digits)

The specific OutOfStockException gets special handling; all other order problems fall through to the OrderException catch block. The payment exception keeps the original IllegalArgumentException as its cause.

6. Best Practices

  • Name it after the problem and end the name with Exception: InsufficientBalanceException, not BalanceError.
  • Provide the standard constructors: message, and message plus cause.
  • Always keep the cause when wrapping another exception.
  • Add fields for data callers need (amount, ID, error code) instead of making them parse the message.
  • Reuse standard exceptions when they fit (IllegalArgumentException, IllegalStateException); do not create custom exceptions for everything.
  • Keep exceptions immutable and lightweight; avoid heavy work in constructors.
  • Do not extend Throwable or Error directly.
  • Add a serialVersionUID if your exceptions may be serialized (for example across remote calls).

7. Summary

  • Create a custom exception by extending Exception (checked) or RuntimeException (unchecked).
  • Pass the message (and cause) to super(...) in its constructors.
  • Add fields to carry useful details about the failure.
  • Organise related exceptions in a hierarchy so callers can catch at the right level.
  • Use custom exceptions for domain-specific problems; reuse standard ones otherwise.

Related Tutorials