Custom Exceptions

Define your own exception classes to represent domain-specific failures clearly, choosing checked or unchecked based on whether callers can reasonably be expected to recover.

Java's built-in exceptions (IllegalArgumentException, IOException, and friends) cover generic situations, but they can't express domain meaning like InsufficientFundsException or OrderAlreadyShippedException. Defining your own exception classes lets calling code catch and respond to precisely the failure that matters, with a name that documents intent.

Why Create Custom Exceptions

A custom exception type turns a vague failure ('an IllegalStateException was thrown somewhere') into a specific, catchable, self-documenting one ('an InsufficientFundsException was thrown'). It also lets you attach domain-specific data — an account ID, a requested amount — directly on the exception object instead of parsing it out of a message string.

Extending Exception (Checked)

Extending java.lang.Exception (but not RuntimeException) creates a checked exception: callers must catch it or declare it with throws. Choose this when the failure is an expected, often-recoverable condition that callers should be forced to consciously address — for example, a business rule violation in a workflow the caller should handle explicitly.

public class InsufficientFundsException extends Exception {
    private final double shortfall;

    public InsufficientFundsException(String message, double shortfall) {
        super(message);
        this.shortfall = shortfall;
    }

    public double getShortfall() {
        return shortfall;
    }
}

Extending RuntimeException (Unchecked)

Extending RuntimeException creates an unchecked exception: no throws declaration is required, and callers are not forced to catch it. Choose this for programming errors or conditions that usually indicate a bug or an unrecoverable situation for most callers — where forcing every intermediate layer to declare or catch it would just add ceremony.

public class OrderAlreadyShippedException extends RuntimeException {
    private final String orderId;

    public OrderAlreadyShippedException(String orderId) {
        super("Order " + orderId + " has already shipped and cannot be modified");
        this.orderId = orderId;
    }

    public String getOrderId() {
        return orderId;
    }
}

Standard Constructors

By convention, a custom exception should provide the same four constructor shapes its superclass (Throwable) provides, so it composes well with cause-chaining and logging tools that expect them: no-arg, message-only, message-and-cause, and cause-only.

The message-and-cause constructor is the one used most often in practice — it's what lets you wrap a lower-level exception (like an IOException from a failed network call) while adding your own explanation.

public class ServiceUnavailableException extends RuntimeException {
    public ServiceUnavailableException() {
        super();
    }
    public ServiceUnavailableException(String message) {
        super(message);
    }
    public ServiceUnavailableException(String message, Throwable cause) {
        super(message, cause);
    }
    public ServiceUnavailableException(Throwable cause) {
        super(cause);
    }
}

Adding Domain-Specific Data

Beyond a message, a custom exception can carry structured fields relevant to the failure — an account number, an attempted value, an error code — exposed via getters. This lets catching code make programmatic decisions instead of parsing a human-readable string.

try {
    account.withdraw(500.00);
} catch (InsufficientFundsException e) {
    System.out.printf("Need %.2f more to complete this withdrawal%n", e.getShortfall());
}

Exception Naming Conventions

Custom exception class names should end in Exception (by strong convention, not a compiler rule) and describe the specific failure, not the general category — InsufficientFundsException, not AccountException or BadThingHappenedException. Avoid vague or overly generic names; a reader should understand the failure from the class name alone.

Basic Syntax: Throwing and Catching

public class Account {
    private double balance;

    public Account(double balance) { this.balance = balance; }

    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(
                "Cannot withdraw " + amount + " from balance " + balance,
                amount - balance);
        }
        balance -= amount;
    }
}

public class Bank {
    public static void main(String[] args) {
        Account acc = new Account(100.0);
        try {
            acc.withdraw(150.0);
        } catch (InsufficientFundsException e) {
            System.out.println(e.getMessage() + " (short by " + e.getShortfall() + ")");
        }
    }
}

Practical Usage: Building an Exception Hierarchy

Larger applications often define one abstract or common base exception per subsystem, with more specific exceptions extending it, so callers can catch broadly (the base type) or narrowly (a specific subtype) depending on what they need.

public abstract class PaymentException extends RuntimeException {
    protected PaymentException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class CardDeclinedException extends PaymentException {
    public CardDeclinedException(String message) { super(message, null); }
}

public class PaymentGatewayTimeoutException extends PaymentException {
    public PaymentGatewayTimeoutException(Throwable cause) {
        super("Payment gateway timed out", cause);
    }
}

// Caller can catch specifically or broadly:
// catch (CardDeclinedException e) { ... }
// catch (PaymentException e) { ... }  // catches any payment-related failure</

Edge Cases

Because Throwable implements Serializable, custom exceptions inherit that too; if your project relies on serializing exceptions (e.g. across a network boundary or into a distributed cache), it's good practice to declare an explicit serialVersionUID to avoid deserialization warnings or incompatibilities across recompiles.

Overriding getMessage() is possible but rarely necessary; prefer building the final message string once in the constructor via super(message) so the message is available consistently (including from getLocalizedMessage() and printStackTrace()).

Avoid making custom exceptions do meaningful work in their constructors (like I/O or logging) — exception construction should stay cheap and side-effect-free; it can happen even when the exception is ultimately caught and ignored somewhere.

Complete Example

public class OrderValidationException extends Exception {
    private static final long serialVersionUID = 1L;
    private final String orderId;

    public OrderValidationException(String orderId, String message) {
        super(message);
        this.orderId = orderId;
    }

    public OrderValidationException(String orderId, String message, Throwable cause) {
        super(message, cause);
        this.orderId = orderId;
    }

    public String getOrderId() { return orderId; }
}

class OrderService {
    void submit(String orderId, int quantity) throws OrderValidationException {
        if (quantity <= 0) {
            throw new OrderValidationException(orderId,
                "Quantity must be positive, got " + quantity);
        }
        System.out.println("Order " + orderId + " submitted.");
    }
}

public class Main {
    public static void main(String[] args) {
        try {
            new OrderService().submit("ORD-1", -3);
        } catch (OrderValidationException e) {
            System.out.println("Rejected order " + e.getOrderId() + ": " + e.getMessage());
        }
    }
}

Mental Model

A custom exception is a specialized, labeled envelope for bad news. The label (class name) tells the reader what kind of bad news it is before they even open it; the envelope's contents (fields plus the inherited message and cause) tell them the details. Extending Exception makes the mail carrier require a signature (checked, must be handled/declared); extending RuntimeException lets it be delivered without one (unchecked).

Quick Reference

class MyException extends Exception { ... }         // checked
class MyException extends RuntimeException { ... }   // unchecked

Provide constructors: (), (msg), (msg, cause), (cause)
Name ends in "Exception"; name describes the specific failure
Add domain fields + getters as needed
Consider a shared abstract base for a related family of exceptions
Add serialVersionUID if serialization matters

Key Takeaways

Custom exceptions give failures specific, meaningful, catchable names and can carry structured domain data beyond a plain message.

Extend Exception for checked exceptions (callers forced to handle/declare) and RuntimeException for unchecked ones (usually programming errors or unrecoverable conditions).

Provide the standard constructor set, especially message-and-cause, so wrapping and chaining works smoothly.

Name custom exceptions after the specific failure, ending in Exception, and consider a shared base type for a related family.

Keep exception construction cheap and side-effect-free; add serialVersionUID if instances may be serialized.

public class BadException extends Exception { public BadException(String msg) { // super() not called with msg — message lost } } public class BadException extends Exception { public BadException(String msg) { super(msg); } }

Forgetting to pass the message to the superclass constructor means getMessage() returns null even though a message string was supplied.

class DataException extends RuntimeException {} // vague, no context class InvalidCustomerIdException extends RuntimeException { InvalidCustomerIdException(String id) { super("Invalid customer id: " + id); } }

A vague exception name and empty body forces every catcher to inspect the message string to know what actually went wrong; a specific, descriptive exception is self-documenting.

catch (SQLException e) { throw new AppException("db failure"); // cause dropped } catch (SQLException e) { throw new AppException("db failure", e); // cause preserved }

When wrapping a caught exception in a custom one, always pass the original as the cause so the root stack trace isn't lost.

What determines whether a custom exception is checked or unchecked?

Which class it extends: extending Exception (but not RuntimeException) makes it checked; extending RuntimeException makes it unchecked.

Why should a custom exception class provide a message-and-cause constructor?

So it can wrap a lower-level exception it's translating, preserving the original's stack trace as the cause while adding a higher-level explanation.

Give an example of when a checked custom exception is more appropriate than an unchecked one.

A recoverable, expected business condition the caller should be forced to consciously handle, such as InsufficientFundsException in a banking withdrawal flow.

Why might an application define an abstract base exception for a subsystem with several concrete subtypes?

So callers can choose to catch broadly (the shared base type, for any failure in that subsystem) or narrowly (a specific subtype) depending on how they need to respond.

What convention applies to naming custom exception classes?

The name should end in Exception and describe the specific failure rather than a vague general category, so the class name alone documents intent.

Interview: Why do Throwable-derived classes implement Serializable, and why does that matter for custom exceptions?

Throwable implements Serializable so exceptions can cross serialization boundaries (e.g. RMI); custom exceptions inherit this, so it's good practice to declare an explicit serialVersionUID to avoid version-compatibility warnings if instances might be serialized.

Write a checked custom exception ExpiredCouponException carrying the coupon code and expiry date, plus a method that throws it when a coupon is used past its expiry date.

import java.time.LocalDate; class ExpiredCouponException extends Exception { private final String code; private final LocalDate expiredOn; ExpiredCouponException(String code, LocalDate expiredOn) { super("Coupon " + code + " expired on " + expiredOn); this.code = code; this.expiredOn = expiredOn; } String getCode() { return code; } LocalDate getExpiredOn() { return expiredOn; } } class CouponService { void redeem(String code, LocalDate expiry) throws ExpiredCouponException { if (expiry.isBefore(LocalDate.now())) { throw new ExpiredCouponException(code, expiry); } System.out.println("Coupon " + code + " redeemed."); } }

The exception is checked (extends Exception), so CouponService.redeem must declare throws ExpiredCouponException since it doesn't catch it internally.

Does this compile without a throws declaration or try/catch around loadConfig()? Why?

Yes. ConfigException extends RuntimeException, making it unchecked, so neither loadConfig nor main is required to declare or catch it.

Write an abstract base exception ValidationException (unchecked) with two concrete subclasses, MissingFieldException and OutOfRangeException, and show catching each specifically as well as catching the base type.

abstract class ValidationException extends RuntimeException { protected ValidationException(String message) { super(message); } } class MissingFieldException extends ValidationException { MissingFieldException(String field) { super("Missing required field: " + field); } } class OutOfRangeException extends ValidationException { OutOfRangeException(String field, Object value) { super(field + " out of range: " + value); } } void validate(String field, Integer value) { if (value == null) throw new MissingFieldException(field); if (value < 0 || value > 100) throw new OutOfRangeException(field, value); } public static void main(String[] args) { try { validate("age", -5); } catch (MissingFieldException e) { System.out.println("Specific: " + e.getMessage()); } catch (ValidationException e) { System.out.println("General: " + e.getMessage()); } }

The OutOfRangeException thrown isn't a MissingFieldException, so it falls through to the broader ValidationException catch and prints 'General: age out of range: -5'.

class ConfigException extends RuntimeException {
    ConfigException(String msg) { super(msg); }
}

void loadConfig() {
    throw new ConfigException("missing key: db.url");
}

public static void main(String[] args) {
    loadConfig();
}