Skip to content
intermediatePhase 12 · Java Exceptions

Custom Exceptions

Create your own exception classes for domain-specific error handling.

30m
1 problems
Topic Progress0%

Creating Custom Exceptions

Creating Custom Exceptions

Custom exceptions let you create domain-specific error types that make your code more expressive and easier to debug. Instead of throwing generic Exception or RuntimeException, you throw exceptions that describe exactly what went wrong in your business domain.

Steps to create a custom exception:

  1. Create a class that extends Exception (checked) or RuntimeException (unchecked)
  2. Add constructors that accept messages, causes, or both
  3. Optionally add custom fields for additional context
// Simple custom checked exception
public class InsufficientFundsException extends Exception {
    private double amount;
    private double balance;

    public InsufficientFundsException(double amount, double balance) {
        super("Insufficient funds: requested " + amount + ", available " + balance);
        this.amount = amount;
        this.balance = balance;
    }

    public double getAmount() { return amount; }
    public double getBalance() { return balance; }
    public double getDeficit() { return amount - balance; }
}

// Custom unchecked exception
public class InvalidEmailException extends RuntimeException {
    private String email;

    public InvalidEmailException(String email) {
        super("Invalid email address: " + email);
        this.email = email;
    }

    public InvalidEmailException(String email, String message) {
        super(message);
        this.email = email;
    }

    public String getEmail() { return email; }
}

// Usage
public class BankAccount {
    private double balance;

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

public class UserRegistration {
    public void register(String email) {
        if (email == null || !email.contains("@")) {
            throw new InvalidEmailException(email);
        }
        // Register user
    }
}

Best practices:

  • Always provide a message that explains what went wrong
  • Include relevant data (the values that caused the error)
  • Provide multiple constructors (message-only, message+cause, cause-only)
  • Name exceptions descriptively with an Exception suffix
  • Make custom exceptions serializable if they might cross process boundaries

Choosing Base Class

Choosing Between Exception and RuntimeException

One of the most important decisions when creating a custom exception is whether it should be checked (extend Exception) or unchecked (extend RuntimeException). This choice affects how callers must handle your exception.

Extend Exception (checked) when:

  • Callers can reasonably recover from the error
  • The exception is part of the normal API contract
  • You want to force callers to handle the error
  • Examples: business rule violations, validation errors, resource not found

Extend RuntimeException (unchecked) when:

  • The exception indicates a programming bug
  • Callers cannot meaningfully recover
  • The exception is unexpected and should not happen in correct code
  • Examples: illegal arguments, null values, state violations
// Checked exception - caller must handle
public class OrderNotFoundException extends Exception {
    private String orderId;

    public OrderNotFoundException(String orderId) {
        super("Order not found: " + orderId);
        this.orderId = orderId;
    }

    public String getOrderId() { return orderId; }
}

// Unchecked exception - programming error
public class InvalidOrderStateException extends RuntimeException {
    private String currentState;
    private String attemptedAction;

    public InvalidOrderStateException(String currentState, String attemptedAction) {
        super("Cannot " + attemptedAction + " order in state: " + currentState);
        this.currentState = currentState;
        this.attemptedAction = attemptedAction;
    }
}

// Usage shows the difference
public class OrderService {

    // Checked - forces caller to handle
    public Order findOrder(String orderId) throws OrderNotFoundException {
        Order order = orderRepository.get(orderId);
        if (order == null) {
            throw new OrderNotFoundException(orderId);
        }
        return order;
    }

    // Unchecked - caller doesn't need to handle
    public void cancelOrder(Order order) {
        if (order.getStatus() == Status.SHIPPED) {
            throw new InvalidOrderStateException("SHIPPED", "cancel");
        }
        order.setStatus(Status.CANCELLED);
    }
}

// Caller code
public void clientCode() {
    // Must catch OrderNotFoundException (checked)
    try {
        Order order = findOrder("123");
        cancelOrder(order);
    } catch (OrderNotFoundException e) {
        System.out.println("Could not find order: " + e.getOrderId());
    }
    // No need to catch InvalidOrderStateException (unchecked)
}

Decision framework:

  • Is it a normal part of your API? → Checked
  • Does it indicate a bug? → Unchecked
  • Should callers be forced to handle it? → Checked
  • Would catching it be surprising? → Unchecked

Adding Context

Adding Custom Fields and Methods

Custom exceptions become truly powerful when they carry domain-specific context. By adding custom fields and methods, you provide callers with detailed information about what went wrong, enabling better error handling and debugging.

// Rich custom exception with multiple context fields
public class ValidationException extends Exception {
    private String fieldName;
    private Object rejectedValue;
    private String validationRule;

    public ValidationException(String fieldName, Object rejectedValue, String validationRule) {
        super(String.format("Validation failed for '%s': %s. Rejected value: %s",
                fieldName, validationRule, rejectedValue));
        this.fieldName = fieldName;
        this.rejectedValue = rejectedValue;
        this.validationRule = validationRule;
    }

    public String getFieldName() { return fieldName; }
    public Object getRejectedValue() { return rejectedValue; }
    public String getValidationRule() { return validationRule; }
}

// Exception hierarchy for different error types
public class ApiException extends RuntimeException {
    private int statusCode;
    private String errorCode;

    public ApiException(int statusCode, String errorCode, String message) {
        super(message);
        this.statusCode = statusCode;
        this.errorCode = errorCode;
    }

    public int getStatusCode() { return statusCode; }
    public String getErrorCode() { return errorCode; }
}

public class ResourceNotFoundException extends ApiException {
    private String resourceType;
    private String resourceId;

    public ResourceNotFoundException(String resourceType, String resourceId) {
        super(404, "NOT_FOUND", resourceType + " not found: " + resourceId);
        this.resourceType = resourceType;
        this.resourceId = resourceId;
    }
}

public class DuplicateResourceException extends ApiException {
    public DuplicateResourceException(String resourceType, String identifier) {
        super(409, "DUPLICATE", resourceType + " already exists: " + identifier);
    }
}

// Exception with suppressed exceptions
public class BatchProcessingException extends Exception {
    private List<Exception> errors = new ArrayList<>();
    private int successCount;
    private int failureCount;

    public BatchProcessingException(String message) {
        super(message);
    }

    public void addError(Exception e) {
        errors.add(e);
        failureCount++;
    }

    public void incrementSuccess() {
        successCount++;
    }

    public List<Exception> getErrors() { return errors; }
    public int getSuccessCount() { return successCount; }
    public int getFailureCount() { return failureCount; }
    public int getTotalCount() { return successCount + failureCount; }
}

// Usage
public class UserService {
    public User createUser(String name, String email, int age) throws ValidationException {
        if (name == null || name.trim().isEmpty()) {
            throw new ValidationException("name", name, "must not be empty");
        }
        if (!email.contains("@")) {
            throw new ValidationException("email", email, "must be valid email");
        }
        if (age < 0 || age > 150) {
            throw new ValidationException("age", age, "must be between 0 and 150");
        }
        return new User(name, email, age);
    }
}

// Catching with full context
public void handleUserCreation() {
    try {
        createUser("", "bad-email", -5);
    } catch (ValidationException e) {
        System.out.println("Field: " + e.getFieldName());
        System.out.println("Rule: " + e.getValidationRule());
        System.out.println("Rejected: " + e.getRejectedValue());
    }
}

Key principles:

  • Include the data that caused the error (not just a message)
  • Create exception hierarchies for related errors
  • Use builder patterns for complex exceptions with many fields
  • Consider serialization for distributed systems

Practice Problems

0/1solved
Custom Insufficient Balance Exception

Create a custom checked exception `InsufficientBalanceException` with fields for `requestedAmount`, `availableBalance`, and `accountNumber`. Include a method `getDeficit()` that returns the difference. Then create a `BankAccount` class with a `withdraw` method that throws this exception when the balance is insufficient.

Solution
public class InsufficientBalanceException extends Exception {
    private double requestedAmount;
    private double availableBalance;
    private String accountNumber;

    public InsufficientBalanceException(double requestedAmount, double availableBalance, String accountNumber) {
        super(String.format("Insufficient balance in account %s: requested %.2f, available %.2f",
                accountNumber, requestedAmount, availableBalance));
        this.requestedAmount = requestedAmount;
        this.availableBalance = availableBalance;
        this.accountNumber = accountNumber;
    }

    public double getRequestedAmount() { return requestedAmount; }
    public double getAvailableBalance() { return availableBalance; }
    public String getAccountNumber() { return accountNumber; }
    public double getDeficit() { return requestedAmount - availableBalance; }
}

public class BankAccount {
    private String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }

    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            throw new InsufficientBalanceException(amount, balance, accountNumber);
        }
        balance -= amount;
    }
}

Quiz

1. When should a custom exception extend RuntimeException instead of Exception?

Question 1 options

2. What is the benefit of adding custom fields to a custom exception?

Question 2 options

3. What is the primary purpose of Custom Exceptions?

Question 3 options

4. What is a common mistake when implementing Custom Exceptions?

Question 4 options

Flashcards

Question

When should you create a custom exception vs using a built-in one?

Answer

Create a custom exception when no built-in exception accurately represents your domain-specific error. Custom exceptions provide meaningful names, carry relevant context, and make error handling more expressive.

Question

What constructors should a well-designed custom exception include?

Answer

At minimum: a message-only constructor, a message+cause constructor (for exception chaining), and a cause-only constructor. Optionally include constructors for custom fields.

Question

What is Custom Exceptions?

Answer

Custom Exceptions is a key concept in Java programming.

Question

When to use Custom Exceptions?

Answer

Use Custom Exceptions when building production systems that require reliability, scalability, and maintainability.

Question

Custom Exceptions best practices

Answer

Follow SOLID principles, write clean code, test thoroughly, document decisions, and monitor in production.

Revision Notes

Key Takeaways

  • 1.Custom exceptions make error handling more expressive and domain-specific
  • 2.Extend Exception for recoverable errors (checked), RuntimeException for bugs (unchecked)
  • 3.Include relevant data fields in custom exceptions for better debugging
  • 4.Provide multiple constructors: message, message+cause, and cause-only

Interview Tips

  • Be ready to design an exception hierarchy for a given domain (e.g., e-commerce, banking)
  • Explain when you would use checked vs unchecked custom exceptions
  • Discuss exception chaining and why preserving the original cause matters
  • Talk about best practices: naming, constructors, serialization, and context fields

Cheat Sheet

Custom Exceptions Cheat Sheet

Creating Custom Exceptions

public class MyException extends Exception {
    private String context;
    public MyException(String msg, String context) {
        super(msg);
        this.context = context;
    }
}

Checked vs Unchecked

Extend Exception → checked, callers must handle
Extend RuntimeException → unchecked, indicates bugs

Best Practices

  • Always include a descriptive message
  • Add custom fields for context
  • Provide multiple constructors
  • Name with Exception suffix
  • Create hierarchies for related errors