Creating Custom Exceptions in Java with Examples
Introduction
Java's built-in exceptions describe general problems - a bad argument, a missing file, a
(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 -
OUTPUT of the above code
Explanation of the code
Creating an unchecked custom exception
Extend
OUTPUT of the above code
The constructor here builds the message itself from the bad value, so every
OUTPUT of the above code
The fields are
Exception chaining - keeping the cause
Custom exceptions often wrap a lower-level exception, translating it into terms the caller understands - for example, an
OUTPUT of the above code
The caller deals with one meaningful exception,
OUTPUT of the above code
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
OrderNotFoundExceptionsays far more in a log or stack trace than a genericRuntimeException.
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());
}
}
}
}
Withdrew 200.0, balance now 300.0 Declined - Account AC-1001: cannot withdraw 450.0, balance is 300.0
super(message)passes the message toException, which stores it and returns it fromgetMessage(). Everything else - the stack trace,toString(),printStackTrace()- is inherited.- Because
InsufficientFundsExceptionextendsException,withdraw()must declare it withthrows, 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.
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());
}
}
}
tom.jones@javabrahman.com Not a valid email address: 'tom.jones@'
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());
}
}
}
}
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
final and there are only getters - exceptions should be immutable, as they may be passed around, logged and rethrown.
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());
}
}
}
Error : Cannot load settings from app-settings.properties Cause : java.nio.file.NoSuchFileException: app-settings.properties
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());
}
}
}
}
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, notOrderException2. - 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.
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.Core Java Tutorials
throw vs throws in Java with ExamplesClick to read tutorial: throw vs throws in Java with Examples Java try-with-resources and AutoCloseable ExplainedClick to read tutorial: Java try-with-resources and AutoCloseable Explained Checked vs Unchecked Exceptions in JavaClick to read tutorial: Checked vs Unchecked Exceptions in Java Java try, catch, finally and Multi-catch with ExamplesClick to read tutorial: Java try, catch, finally and Multi-catch with Examples Java Exception Handling Best PracticesClick to read tutorial: Java Exception Handling Best Practices Java Math Class: Essential Methods with ExamplesClick to read tutorial: Java Math Class: Essential Methods with Examples
throw vs throws in Java with ExamplesClick to read tutorial: throw vs throws in Java with Examples Java try-with-resources and AutoCloseable ExplainedClick to read tutorial: Java try-with-resources and AutoCloseable Explained Checked vs Unchecked Exceptions in JavaClick to read tutorial: Checked vs Unchecked Exceptions in Java Java try, catch, finally and Multi-catch with ExamplesClick to read tutorial: Java try, catch, finally and Multi-catch with Examples Java Exception Handling Best PracticesClick to read tutorial: Java Exception Handling Best Practices Java Math Class: Essential Methods with ExamplesClick to read tutorial: Java Math Class: Essential Methods with Examples