⚡ Section 7 · Question #13

Can you create custom exceptions

A custom exception is a standard Java class that inherits from either:


🟢 Junior Level

Yes. In Java, creating custom exceptions is a standard and recommended practice for modeling domain-specific failures and business invariant violations.

A custom exception is a standard Java class that inherits from either:

  • java.lang.RuntimeException (for unchecked business/application exceptions, recommended for modern enterprise code).
  • java.lang.Exception (for checked exceptions that require mandatory compiler handling).

Custom exceptions separate technical infrastructure failures from business events (e.g., throwing a meaningful OrderNotFoundException instead of an ambiguous IllegalArgumentException).

The 4 Canonical Constructors Rule

To ensure your custom exception integrates cleanly with Java diagnostic tools and exception chaining, implement the four standard constructors matching Throwable:

public class InsufficientFundsException extends RuntimeException {

    // 1. Default no-arg constructor
    public InsufficientFundsException() {
        super();
    }

    // 2. Constructor with descriptive message
    public InsufficientFundsException(String message) {
        super(message);
    }

    // 3. Constructor with message and root cause (Exception Chaining)
    public InsufficientFundsException(String message, Throwable cause) {
        super(message, cause);
    }

    // 4. Constructor with root cause only
    public InsufficientFundsException(Throwable cause) {
        super(cause);
    }
}

Basic Code Example

import java.math.BigDecimal;

public class BankAccount {
    private BigDecimal balance;

    public BankAccount(BigDecimal initialBalance) {
        this.balance = initialBalance;
    }

    public void withdraw(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            // Standard JDK exception for invalid arguments
            throw new IllegalArgumentException("Withdrawal amount must be strictly positive");
        }
        if (balance.compareTo(amount) < 0) {
            // Custom domain exception for business invariant violation
            throw new InsufficientFundsException(
                "Cannot withdraw %s: current balance is %s".formatted(amount, balance)
            );
        }
        this.balance = this.balance.subtract(amount);
    }
}

Standard Exceptions vs. Custom Exceptions

Do not create custom exceptions unnecessarily. Prefer standard JDK exceptions when the semantic fit is exact:

  • IllegalArgumentException: Invalid method argument (e.g., negative timeout, invalid format).
  • IllegalStateException: Object state is inappropriate for the invocation (e.g., reading from a closed socket).
  • NullPointerException: Unexpected null argument or reference.
  • UnsupportedOperationException: Unimplemented or unsupported operation (e.g., modifying List.of()).

Create a custom exception when:

  1. The calling code needs to selectively catch and handle this specific failure.
  2. The failure represents a core domain business rule (UserBlockedException, PromoCodeExpiredException).
  3. You need to attach structured domain metadata (order ID, user ID, error code).

🟡 Middle Level

Contextual Exceptions (Exceptions as Data Carriers)

In modern enterprise applications, simple string concatenation in message makes automated parsing and client error reporting fragile. Custom exceptions should act as typed DTOs carrying structured metadata:

import java.time.Instant;
import java.util.Collections;
import java.util.Map;

public class BusinessException extends RuntimeException {
    private final String errorCode;
    private final Instant timestamp;
    private final Map<String, Object> details;

    public BusinessException(String errorCode, String message, Map<String, Object> details) {
        super(message);
        this.errorCode = errorCode;
        this.details = details != null ? Map.copyOf(details) : Collections.emptyMap();
        this.timestamp = Instant.now();
    }

    public BusinessException(String errorCode, String message, Throwable cause, Map<String, Object> details) {
        super(message, cause);
        this.errorCode = errorCode;
        this.details = details != null ? Map.copyOf(details) : Collections.emptyMap();
        this.timestamp = Instant.now();
    }

    public String getErrorCode() { return errorCode; }
    public Instant getTimestamp() { return timestamp; }
    public Map<String, Object> getDetails() { return details; }
}

Designing an Application Exception Hierarchy

Rather than scattering unrelated exception classes across packages, structure a cohesive hierarchy rooted in a common module base class:

RuntimeException
    └── ApplicationException (Abstract base class)
            ├── DomainException (Business rules)
            │       ├── EntityNotFoundException
            │       │       └── UserNotFoundException
            │       └── PaymentValidationException
            └── InfrastructureException (Third-party integrations)
                    ├── ExternalGatewayTimeoutException
                    └── S3StorageUnavailableException

Serialization & serialVersionUID

Because java.lang.Throwable implements java.io.Serializable, exceptions transmitted over remote RPC (RMI, JMX, Hazelcast, or distributed session replication) require an explicit serialVersionUID:

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

Omitting serialVersionUID risks java.io.InvalidClassException during deserialization if compiler-generated hashes differ between service builds.

Spring Boot REST API Integration (RFC 9457 ProblemDetail)

In microservices, custom domain exceptions are mapped centrally to standardized RFC 9457 ProblemDetail responses using @RestControllerAdvice:

package com.example.advice;

import com.example.exception.InsufficientFundsException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.net.URI;
import java.time.Instant;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(InsufficientFundsException.class)
    public ProblemDetail handleInsufficientFunds(InsufficientFundsException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_REQUEST,
            ex.getMessage()
        );
        problem.setTitle("Insufficient Funds");
        problem.setType(URI.create("https://api.mybank.com/errors/insufficient-funds"));
        problem.setProperty("timestamp", Instant.now());
        return problem;
    }
}

🔴 Senior Level

High-Throughput Stackless Exceptions

In HotSpot JVM, the primary cost of creating an exception is the native C++ call to Throwable.fillInStackTrace(), which suspends thread execution and walks every frame of the thread’s call stack.

In high-throughput services (handling 100k+ RPS), generating stack traces for predictable business failures severely degrades CPU performance.

Solution 1: Protected 4-Argument Constructor (Java 7+)

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

Solution 2: Overriding fillInStackTrace()

public class OptimizedValidationException extends RuntimeException {
    public OptimizedValidationException(String message) {
        super(message);
    }

    @Override
    public synchronized Throwable fillInStackTrace() {
        return this; // Bypasses native JVM stack frame traversal!
    }
}

Result: Exception instantiation executes 30x–50x faster with minimal GC allocation pressure.

The Singleton Exception Anti-Pattern

Developers sometimes attempt to eliminate allocation overhead by caching static exception instances:

// ❌ DANGEROUS ANTI-PATTERN:
public static final MyException GENERIC_ERROR = new MyException("Validation failed");

Why This Is Detrimental:

  1. Misleading Stack Traces: If writableStackTrace is enabled, the stack trace captured reflects the classloading initialization point, not the actual point of failure.
  2. Concurrency Data Corruption: If multiple concurrent threads call GENERIC_ERROR.initCause(...) or addSuppressed(), race conditions and IllegalStateException crashes will occur.
  3. Rule: Singleton exceptions are only safe if they are strictly immutable, stackless (writableStackTrace = false), and forbidden from chaining causes.

Architectural Anti-Pattern: Leaking Java Classes Across Microservice Boundaries

Serializing Java exception objects across HTTP, gRPC, or Kafka boundaries to downstream services creates tight classpath coupling:

  • If Service B lacks Service A’s custom exception .class in its classpath, deserialization crashes with ClassNotFoundException or NoClassDefFoundError.
  • Architectural Solution: Translate exceptions at service boundaries into language-agnostic contracts: JSON (RFC 9457 ProblemDetail), Protobuf status payloads, or gRPC status codes (Status.NOT_FOUND).

Security: CWE-209 & PII Masking

Never include Personally Identifiable Information (PII), credit card numbers, passwords, or authentication tokens in exception messages:

// ❌ SEVERE VULNERABILITY (CWE-209 / GDPR violation):
throw new AuthenticationException("Failed for user " + user + " with token " + secretToken);

// ✅ SAFE & COMPLIANT:
throw new AuthenticationException("Authentication failed for principalId: " + principalId);

4 Tricky Questions

1. Why should custom exceptions implement a constructor accepting Throwable cause?

Answer:
To support Exception Chaining. This allows wrapping low-level technical infrastructure exceptions (e.g., SQLException) into high-level business domain exceptions (OrderProcessingException) without obliterating the underlying root cause and its stack trace.

2. How can you optimize a custom exception that is thrown hundreds of thousands of times per second?

Answer:
Disable call stack generation either by invoking the 4-argument constructor super(message, cause, false, false) with writableStackTrace = false, or by overriding public synchronized Throwable fillInStackTrace() to return this. This bypasses the expensive native HotSpot call stack traversal, accelerating instantiation by 30x–50x.

3. Why is creating a public static final exception constant (public static final MyException ERROR = new MyException()) dangerous?

Answer:
If the exception records a stack trace, it records only the stack trace of when the class was loaded by the ClassLoader, rendering production debugging impossible. Furthermore, if concurrent threads attempt to mutate the exception by calling initCause() or addSuppressed(), race conditions or IllegalStateException errors will occur.

4. What is the fundamental architectural difference between extending Exception vs RuntimeException when creating a custom exception?

Answer:
Extending Exception creates a Checked Exception, forcing callers at compile time to enclose the call in try-catch or propagate it via throws. Extending RuntimeException creates an Unchecked Exception, which bubbles up call stacks freely without cluttering method signatures and integrates cleanly with Java Streams, functional interfaces, and Spring’s @RestControllerAdvice.


🎯 Interview Cheat Sheet

30-Second Summary

Custom exceptions in Java extend RuntimeException (for unchecked domain/business errors) or Exception (for recoverable checked conditions).

Best Practices:

  1. Provide all 4 canonical constructors to support message and cause propagation.
  2. Include structured typed metadata (errorCode, timestamp, details) instead of unparseable formatted strings.
  3. Use stackless exceptions (writableStackTrace = false) for highload hot paths.
  4. Never leak custom exception classes across microservices; serialize them as RFC 9457 ProblemDetail JSON at API boundaries.
  5. Never log passwords, tokens, or PII in exception messages (CWE-209).

Standard Exceptions to Use Before Creating Custom Ones

  • IllegalArgumentException: Invalid input parameter.
  • IllegalStateException: Inappropriate object state.
  • NullPointerException: Unexpected null reference.
  • UnsupportedOperationException: Unimplemented or immutable operation.

Red Flags (What to Avoid)

  • ❌ “Custom exceptions should inherit directly from Throwable.” (Must extend Exception or RuntimeException).
  • ❌ “Create a new custom exception class for every single error scenario.” (Causes class explosion; group errors logically).
  • ❌ “Log raw passwords and tokens inside exception messages to debug auth issues.” (Critical security and GDPR compliance violation).
  • ❌ “Discard the original exception when rethrowing: throw new CustomException(e.getMessage());.” (Destroys the root cause stack trace; pass e as cause).