Question 13 · Section 7

Can you create custom exceptions?

A custom exception is a regular class that inherits from Exception or RuntimeException.

Language versions: English Russian Ukrainian

Junior Level

Yes, you can!

A custom exception is a regular class that inherits from Exception or RuntimeException.

Custom = created by you for your domain. Standard = from JDK (IOException, IllegalArgumentException). Custom ones carry domain meaning: OrderNotFoundException says more than IllegalArgumentException.

// Checked exception
public class UserNotFoundException extends Exception {
    public UserNotFoundException(String message) {
        super(message);
    }
}

// Unchecked exception
public class InsufficientFundsException extends RuntimeException {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

How to use

public User findUser(Long id) {
    User user = repository.findById(id);
    if (user == null) {
        throw new UserNotFoundException("User not found: " + id);
    }
    return user;
}

// Handling
try {
    findUser(1L);
} catch (UserNotFoundException e) {
    System.out.println(e.getMessage());
}

When to create checked, when unchecked

  • Checked (extends Exception) - if error is expected and recoverable
  • Unchecked (extends RuntimeException) - for business errors and programming errors

Tips

  • Inherit from RuntimeException in most cases
  • Give meaningful names: UserNotFoundException, OrderAlreadyShippedException
  • Call super(message) to pass the message

When NOT to create custom exceptions

  1. Standard is sufficient - IllegalArgumentException("Email invalid") is readable
  2. Exception used only once - no point creating a class for a single throw
  3. “Parallel hierarchy” - don’t create an Exception for every new use case

Middle Level

Exception as DTO

In distributed systems, a custom exception serves as DTO for error transmission:

public class BusinessException extends RuntimeException {
    private final ErrorCode code;
    private final Map<String, Object> context = new HashMap<>();

    public BusinessException(ErrorCode code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException with(String key, Object value) {
        this.context.put(key, value);
        return this;
    }

    public ErrorCode getCode() { return code; }
    public Map<String, Object> getContext() { return context; }
}

Usage:

throw new BusinessException(INSUFFICIENT_FUNDS, "Low balance")
    .with("userId", 123)
    .with("balance", 0.50);

// Advantage of .with(): you can add context at the throw site, // without creating a constructor with 10 parameters. // Lambda-style: throw new BusinessException(code, msg).with(“key”, value)

Domain-Driven Exceptions

In quality architecture, exceptions are divided into levels:

  1. Infrastructure: DatabaseException, NetworkException
  2. Domain: InsufficientFundsException, ProductOutOfStockException

Domain exceptions should be informative - not just “error”, but “user X could not buy product Y”.

The microservices problem

If service A throws OrderNotFoundException, and service B doesn’t have this class in classpath - NoClassDefFoundError.

Solution: at microservice boundaries, exceptions are translated to standard structures (RFC 7807 Problem Details). Custom exceptions live only inside the service.

RFC 7807 - standard JSON format for error messages in HTTP APIs. Defines fields: type, title, status, detail, instance. Spring supports it via ProblemDetail (since Java 17).


Senior Level

Immutable & Stackless Exceptions for Highload

In Highload systems, creating Throwable is expensive due to fillInStackTrace():

public class ValidationException extends RuntimeException {
    public ValidationException(String message) {
        super(message, null, false, false); // writableStackTrace = false
    }
}

Thousands of exceptions per second without CPU load.

Serialization UID

Always declare serialVersionUID:

public class BusinessException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    // ...
}

Without it, when you change the class (add a field) and deserialize an old version from Redis cache - InvalidClassException.

Exception Masking and PII

Custom exception message must not expose sensitive data:

// BAD - password in logs
throw new AuthException("Failed login for user: " + username + " with password: " + password);

// GOOD
throw new AuthException("Authentication failed for user: " + username);

Global Error Handler in Spring Boot

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(BusinessException e) {
        return ResponseEntity.status(400)
            .body(ErrorResponse.of(e.getCode(), e.getMessage(), e.getContext()));
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException e) {
        return ResponseEntity.status(404)
            .body(ErrorResponse.of("USER_NOT_FOUND", e.getMessage()));
    }
}

Diagnostics

  • Log Correlation - exception message should contain data for filtering in ELK
  • Error Codes - each exception should have a unique code (e.g., ERR-001) for localization
  • Micrometer - count each custom exception via metrics

Interview Cheat Sheet

Must know:

  • Custom exception = regular class inheriting from Exception or RuntimeException
  • Inherit from RuntimeException in most cases (less boilerplate)
  • extends Exception - if error is expected and caller can recover
  • In microservices, custom exceptions live only inside the service; at boundary - translate to standard formats (RFC 7807)
  • For highload use writableStackTrace = false to avoid expensive fillInStackTrace()
  • Always declare serialVersionUID for Serializable compatibility

Frequent follow-up questions:

  • When NOT to create a custom exception? - If standard is sufficient (IllegalArgumentException), or exception is used once
  • How to pass context in an exception? - Add fields (orderId, errorCode) or use fluent method .with(key, value)
  • What is Exception Masking? - Exception must not expose sensitive data (passwords, PII) in message
  • How to handle custom exceptions in Spring? - Via @RestControllerAdvice + @ExceptionHandler -> mapping to HTTP statuses

Red flags (NOT to say):

  • “I create an exception for each use case” - This leads to “parallel hierarchy” and code bloat
  • “I pass password/PII in exception message” - Security violation (PCI DSS, GDPR)
  • “Custom exceptions automatically map to HTTP” - Need @ControllerAdvice, otherwise it’s 500

Related topics:

  • [[14. When should you create your own exceptions]]
  • [[15. What is better extend Exception or RuntimeException]]
  • [[17. How to properly log exceptions]]
  • [[2. What is a checked exception and when to use it]]
  • [[3. What is an unchecked exception (Runtime Exception)]]