Creating Custom Exceptions in Java with Examples

Introduction Java's built-in exceptions describe general problems - a bad argument, a missing file, a null reference. But many problems belong to your application's own domain: an account without enough funds, an order for a product that is out of stock, an invalid email address. Custom exceptions let you name these problems, carry the details that go with them, and let callers catch exactly the cases they care about. This tutorial explains how to create custom exceptions in Java. We will start with when a custom exception is worth creating. We will then write a checked custom exception, and an unchecked one. Next we will add fields which carry details about the failure, and support exception chaining with a cause. Lastly, we will organize related exceptions into a small hierarchy, and go through naming and design guidelines.

(Note - This tutorial builds on Checked vs Unchecked ExceptionsClick to read tutorial: Checked vs Unchecked Exceptions in Java and throw vs throwsClick to read tutorial: throw vs throws in Java with Examples.) When should you create a custom exception? Before creating one, check whether a standard exception already fits - IllegalArgumentException, IllegalStateException, UnsupportedOperationException and IOException cover a great many cases, and every Java developer knows what they mean. A custom exception is worth it when -
  • Callers need to tell it apart - they want to catch this problem and handle it differently from others, such as showing "insufficient funds" to a user.
  • It carries extra information - such as the account number, the balance and the amount requested.
  • It describes a domain problem - a name such as OrderNotFoundException says far more in a log or stack trace than a generic RuntimeException.
Creating a checked custom exception A custom exception is an ordinary class which extends an exception class. Extend Exception to make it checked, so that the compiler forces callers to handle it. By convention, the class name ends in Exception, and it provides constructors which pass a message - and, if needed, a cause - to the superclass.
A checked custom exception
//InsufficientFundsException.java
public class InsufficientFundsException extends Exception {
  public InsufficientFundsException(String message) {
    super(message);
  }
}
Throwing and catching the custom exception
//Account.java
public class Account {
  private final String id;
  private double balance;

  Account(String id, double balance) {
    this.id = id;
    this.balance = balance;
  }

  void withdraw(double amount) throws InsufficientFundsException {
    if (amount > balance) {
      throw new InsufficientFundsException(
          "Account " + id + ": cannot withdraw " + amount + ", balance is " + balance);
    }
    balance -= amount;
  }

  public static void main(String[] args) {
    Account account = new Account("AC-1001", 500);
    double[] requests = {200, 450};
    for (double amount : requests) {
      try {
        account.withdraw(amount);
        System.out.println("Withdrew " + amount + ", balance now " + account.balance);
      } catch (InsufficientFundsException e) {
        System.out.println("Declined - " + e.getMessage());
      }
    }
  }
}
 OUTPUT of the above code
Withdrew 200.0, balance now 300.0
Declined - Account AC-1001: cannot withdraw 450.0, balance is 300.0
Explanation of the code
  • super(message) passes the message to Exception, which stores it and returns it from getMessage(). Everything else - the stack trace, toString(), printStackTrace() - is inherited.
  • Because InsufficientFundsException extends Exception, withdraw() must declare it with throws, and its callers must catch or declare it. Running out of funds is a normal situation that callers should be ready for, so a checked exception fits.
Creating an unchecked custom exception Extend RuntimeException instead, and the exception is unchecked - methods need not declare it, and callers need not catch it. This suits programming errors, broken rules that callers should have checked, and code where forcing every layer to declare the exception would only add noise.
An unchecked custom exception
//InvalidEmailException.java
public class InvalidEmailException extends RuntimeException {
  public InvalidEmailException(String email) {
    super("Not a valid email address: '" + email + "'");
  }
}
Using an unchecked custom exception
//Registration.java
public class Registration {
  static String normalizeEmail(String email) {
    if (email == null || !email.matches("[^@\\s]+@[^@\\s]+\\.[a-zA-Z]{2,}")) {
      throw new InvalidEmailException(email);
    }
    return email.strip().toLowerCase();
  }

  public static void main(String[] args) {
    System.out.println(normalizeEmail("Tom.Jones@JavaBrahman.com"));   // no try needed
    try {
      normalizeEmail("tom.jones@");
    } catch (InvalidEmailException e) {
      System.out.println(e.getMessage());
    }
  }
}
 OUTPUT of the above code
tom.jones@javabrahman.com
Not a valid email address: 'tom.jones@'
The constructor here builds the message itself from the bad value, so every InvalidEmailException gets a consistent, informative message. Adding fields with details An exception is an object, so it can have fields. Storing the details of the failure in fields - not just in the message text - lets the code which catches the exception use them, for example to show a helpful error or decide what to do next.
A custom exception with detail fields
//OutOfStockException.java
public class OutOfStockException extends Exception {
  private final String productCode;
  private final int requested;
  private final int available;

  public OutOfStockException(String productCode, int requested, int available) {
    super("Product " + productCode + ": requested " + requested + ", only " + available + " in stock");
    this.productCode = productCode;
    this.requested = requested;
    this.available = available;
  }

  public String getProductCode() { return productCode; }
  public int getRequested() { return requested; }
  public int getAvailable() { return available; }
  public int getShortfall() { return requested - available; }
}
Using the exception's fields when handling it
//Warehouse.java
import java.util.Map;

public class Warehouse {
  private final Map<String, Integer> stock = Map.of("PEN-01", 40, "LAMP-07", 3);

  void reserve(String code, int quantity) throws OutOfStockException {
    int available = stock.getOrDefault(code, 0);
    if (quantity > available) {
      throw new OutOfStockException(code, quantity, available);
    }
    System.out.println("Reserved " + quantity + " x " + code);
  }

  public static void main(String[] args) {
    Warehouse warehouse = new Warehouse();
    String[][] orders = {{"PEN-01", "25"}, {"LAMP-07", "5"}};
    for (String[] order : orders) {
      try {
        warehouse.reserve(order[0], Integer.parseInt(order[1]));
      } catch (OutOfStockException e) {
        System.out.println(e.getMessage());
        System.out.println("  Suggest: order " + e.getAvailable() + " now and back-order "
            + e.getShortfall() + " of " + e.getProductCode());
      }
    }
  }
}
 OUTPUT of the above code
Reserved 25 x PEN-01
Product LAMP-07: requested 5, only 3 in stock
  Suggest: order 3 now and back-order 2 of LAMP-07
The fields are final and there are only getters - exceptions should be immutable, as they may be passed around, logged and rethrown.
Exception chaining - keeping the cause Custom exceptions often wrap a lower-level exception, translating it into terms the caller understands - for example, an IOException while loading settings becomes a ConfigurationException. Give the class a constructor which accepts a cause, and pass it to super(message, cause). The cause then appears as "Caused by:" in stack traces, and is available from getCause().
A custom exception that wraps a cause
//ConfigurationException.java
public class ConfigurationException extends RuntimeException {
  public ConfigurationException(String message) {
    super(message);
  }

  public ConfigurationException(String message, Throwable cause) {
    super(message, cause);
  }
}
Wrapping an IOException in a custom exception
//SettingsLoader.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class SettingsLoader {
  static String load(String file) {
    try {
      return Files.readString(Path.of(file));
    } catch (IOException e) {
      throw new ConfigurationException("Cannot load settings from " + file, e);
    }
  }

  public static void main(String[] args) {
    try {
      load("app-settings.properties");
    } catch (ConfigurationException e) {
      System.out.println("Error  : " + e.getMessage());
      System.out.println("Cause  : " + e.getCause().getClass().getName() + ": " + e.getCause().getMessage());
    }
  }
}
 OUTPUT of the above code
Error  : Cannot load settings from app-settings.properties
Cause  : java.nio.file.NoSuchFileException: app-settings.properties
The caller deals with one meaningful exception, ConfigurationException, without needing to know whether the settings came from a file, a database or the network - yet the original NoSuchFileException is preserved for whoever investigates the problem. A small exception hierarchy When an application has several related exceptions, give them a common superclass. Callers can then catch one specific exception, or all of them through the superclass.
A hierarchy of related custom exceptions
//PaymentExceptions.java
public class PaymentExceptions {
  static class PaymentException extends Exception {
    PaymentException(String message) { super(message); }
  }
  static class CardDeclinedException extends PaymentException {
    CardDeclinedException(String reason) { super("Card declined: " + reason); }
  }
  static class PaymentTimeoutException extends PaymentException {
    PaymentTimeoutException(int seconds) { super("No response from the bank after " + seconds + "s"); }
  }

  static void pay(int scenario) throws PaymentException {
    switch (scenario) {
      case 1 -> throw new CardDeclinedException("insufficient credit");
      case 2 -> throw new PaymentTimeoutException(30);
      default -> System.out.println("Payment " + scenario + " approved");
    }
  }

  public static void main(String[] args) {
    for (int scenario = 0; scenario <= 2; scenario++) {
      try {
        pay(scenario);
      } catch (PaymentTimeoutException e) {
        System.out.println("Retrying later - " + e.getMessage());
      } catch (PaymentException e) {                       // every other payment problem
        System.out.println("Payment failed - " + e.getMessage());
      }
    }
  }
}
 OUTPUT of the above code
Payment 0 approved
Payment failed - Card declined: insufficient credit
Retrying later - No response from the bank after 30s
pay() declares only the superclass, PaymentException. The caller treats a timeout specially, by retrying, and handles every other payment failure in one block - including any new subclasses added later. (The exceptions are nested classes here only to keep the example in one file; in a real project each would be in its own file.) Guidelines for custom exceptions
  • End the name with Exception, and name the problem: OrderNotFoundException, not OrderException2.
  • Choose checked or unchecked deliberately: checked for conditions the caller can recover from, unchecked for programming errors (see checked vs uncheckedClick to read tutorial: Checked vs Unchecked Exceptions in Java).
  • Never extend Throwable or Error directly - extend Exception, RuntimeException, or one of their subclasses.
  • Provide a cause constructor whenever the exception may wrap another one, and always pass the cause.
  • Write useful messages which include the values involved, and keep detail fields immutable.
  • Do not overdo it - a separate exception class for every error makes code harder to follow. Reuse the standard exceptions when they fit.
Summary In this tutorial we first saw when a custom exception is worth creating, and when a standard one is better. We then created a checked custom exception by extending Exception, and an unchecked one by extending RuntimeException. Next we added detail fields which the catching code used to suggest a back-order. We then supported exception chaining with a cause constructor, wrapping an IOException. Lastly, we built a small hierarchy of payment exceptions, and went through guidelines for designing custom exceptions. In the next tutorial, we will close resources safely with try-with-resourcesClick to read tutorial: Java try-with-resources and AutoCloseable Explained.