Skip to content

Custom Exceptions Interview Questions & Answers

21 questions Updated 2026-06-20 Share:

Java custom exceptions interview questions — when and how to create your own exception classes, checked vs unchecked design, exception chaining, preserving stack traces, and best practices.

Read the in-depth guideJava Custom Exceptions — Design, Chaining & Best Practices(opens in new tab)
21 of 21

A custom exception gives an error a domain-specific name and type that callers can catch precisely. throw new RuntimeException("insufficient funds") forces callers to parse a string or catch everything; throw new InsufficientFundsException(...) lets them write catch (InsufficientFundsException e) and react to that failure alone.

// vague — caller can't distinguish this from any other RuntimeException
throw new RuntimeException("order not found: " + id);

// expressive — a dedicated type the caller can target
throw new OrderNotFoundException(id);

Custom exceptions also let you carry structured data (the offending id, an error code) and document the failure modes of your API in its type signature. Rule of thumb: create one when callers need to react differently to this error or need data from it; otherwise reuse a built-in.

Extend Exception to make it checked (callers must catch or declare it) or RuntimeException to make it unchecked (no compiler obligation). Never extend Throwable or Error directly — those are reserved for the JVM and catch-all framework code.

// checked: caller is forced to handle it
public class ConfigParseException extends Exception { }

// unchecked: a programming/usage error, no catch obligation
public class InvalidStateException extends RuntimeException { }

The choice signals intent: subclass Exception for a recoverable condition the caller should plan for, RuntimeException for a bug or precondition violation. Rule of thumb: pick the superclass first — it encodes whether the error is expected or a defect.

Mirror the four constructors that Exception/RuntimeException themselves expose, so callers can supply a message, a cause, or both:

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

The (String, Throwable) and (Throwable) constructors are the ones people forget, yet they're the most important — they enable exception chaining (wrapping a lower-level cause). Rule of thumb: always include at least the (String message) and (String message, Throwable cause) forms.

Make it checked when the error is a recoverable, expected condition the caller can reasonably handle (a missing file, a failed network call). Make it unchecked when it represents a programming error or violated precondition the caller can't sensibly recover from (null argument, illegal state).

Aspect Checked (extends Exception) Unchecked (extends RuntimeException)
Compiler enforces handling Yes No
Use for Recoverable, anticipated Bugs / preconditions
Method signature Must declare with throws No declaration needed
Caller's typical action Catch & recover/retry Let it propagate, fix the bug

Rule of thumb: "Can the caller do something useful about it?" Yes -> checked (or at least catchable); no -> unchecked.

Critics argue checked exceptions hurt scalability and clutter code: every caller must catch or re-declare them, so adding a checked exception to a method ripples throws clauses up the whole call stack. They also break cleanly with lambdas and streams, whose functional interfaces don't declare checked exceptions.

// a checked exception can't escape a stream lambda directly
files.stream()
     .map(f -> Files.readString(f)); // won't compile — IOException is checked

The result is often swallowing or blanket-wrapping in RuntimeException, defeating the purpose. Modern frameworks (Spring, many libraries) lean almost entirely on unchecked exceptions for this reason. Rule of thumb: know both sides — checked enforces handling, but overusing it leaks implementation and fights functional code.

Exception chaining is wrapping a low-level exception inside a higher-level one while keeping the original as the "cause." It lets you raise a meaningful domain exception without throwing away the root-cause diagnostic information.

try {
  return jdbc.query(sql);
} catch (SQLException e) {
  // wrap, but keep the SQLException as the cause
  throw new RepositoryException("failed to load user " + id, e);
}

The chained cause appears in the stack trace under "Caused by:", so you see both the abstract failure and the SQL error that triggered it. Losing the cause means losing the line that actually broke. Rule of thumb: when translating an exception, always pass the original in as the cause.

Both record a cause; the constructor is the preferred, concise form, while initCause() exists for exceptions whose class predates the cause constructors or doesn't expose one.

// preferred — set the cause at construction
throw new ServiceException("lookup failed", sqlEx);

// fallback — when no cause constructor is available
ServiceException ex = new ServiceException("lookup failed");
ex.initCause(sqlEx);   // can be called only ONCE
throw ex;

initCause() may be called exactly once and only if the cause wasn't already set, otherwise it throws IllegalStateException. getCause() reads it back. Rule of thumb: use the (message, cause) constructor; reach for initCause only when a constructor isn't an option.

Throwable.getCause() returns the underlying exception that was chained in, or null if none was set. It lets handlers and logging code walk the chain to find the root cause.

try {
  service.process();
} catch (ServiceException e) {
  Throwable root = e;
  while (root.getCause() != null) root = root.getCause();
  log.error("root cause: {}", root.getMessage());
}

Most logging frameworks print the full chain automatically (the "Caused by:" lines), so you rarely walk it by hand — but getCause() is how you inspect it programmatically (e.g., to unwrap a known wrapped exception). Rule of thumb: use getCause() to inspect or unwrap; let the logger print the chain.

You lose the original trace by catching an exception and throwing a new one without passing the cause — the new exception's trace starts at the throw site, hiding where it really failed.

// BAD — the original cause and its line are gone
catch (SQLException e) {
  throw new DaoException("query failed");
}

// GOOD — chain the cause; both traces are kept
catch (SQLException e) {
  throw new DaoException("query failed", e);
}

Equally bad is swallowing (catch (Exception e) {}) or logging and rethrowing a fresh exception, which double-logs and detaches the trace. Rule of thumb: when you rethrow, always carry the cause; never create a new exception that drops the original.

Swallowing is catching an exception and doing nothing (or just logging at a low level) so the failure silently disappears. The program continues in a broken state and the bug surfaces far away, with no trace of the origin.

// the worst line in any codebase
try {
  risky();
} catch (Exception e) {
  // empty — failure vanishes
}

If you genuinely must ignore an exception, comment why and at minimum log it with the stack trace. Catching Exception/Throwable broadly is itself a smell — it can hide RuntimeExceptions and even Errors you never meant to suppress. Rule of thumb: never leave a catch block empty; handle, rethrow, or log with the cause.

Add final fields populated through the constructor and exposed via getters. This lets handlers retrieve structured context (an id, an error code) instead of parsing the message string.

public class OrderNotFoundException extends RuntimeException {
  private final long orderId;

  public OrderNotFoundException(long orderId) {
    super("order not found: " + orderId);
    this.orderId = orderId;
  }
  public long getOrderId() { return orderId; }
}

Now a caller can do catch (OrderNotFoundException e) { retry(e.getOrderId()); }. Keep the fields immutable (final) since an exception is a one-shot value object. Rule of thumb: put any data the handler might need into typed fields, not just the message.

The class name should end with Exception and describe the failure clearly, e.g. InsufficientFundsException, OrderNotFoundException, ConfigParseException. Subtypes of Error end with Error.

class InvalidTokenException extends RuntimeException { } // good
class TokenProblem extends RuntimeException { }          // unclear — avoid

The convention makes exceptions instantly recognizable in code and stack traces, and mirrors the JDK's own (IllegalArgumentException, IOException). Name them for the problem, not the throwing method. Rule of thumb: suffix with Exception and name the condition, not the location.

Throwable already implements Serializable, so every exception is serializable by inheritance — important because exceptions cross JVM boundaries (RMI, distributed apps, app servers). To keep serialization stable you should declare a serialVersionUID and ensure any custom fields are themselves serializable.

public class RemoteCallException extends RuntimeException {
  private static final long serialVersionUID = 1L;
  private final String endpoint;   // String is serializable — OK

  public RemoteCallException(String endpoint, Throwable cause) {
    super("call failed: " + endpoint, cause);
    this.endpoint = endpoint;
  }
}

Without an explicit serialVersionUID, the compiler-generated one changes with any edit, breaking deserialization of older instances. Rule of thumb: add a serialVersionUID and keep custom fields serializable (or mark them transient).

Yes — treat an exception as an immutable value object. Set everything through the constructor and make fields final; an exception describes a moment in time and shouldn't be mutated after it's thrown.

public class ValidationException extends RuntimeException {
  private final List<String> errors;

  public ValidationException(List<String> errors) {
    super(errors.size() + " validation error(s)");
    this.errors = List.copyOf(errors);   // defensive, unmodifiable copy
  }
  public List<String> getErrors() { return errors; }
}

Defensively copy mutable inputs (here List.copyOf) so the stored data can't be changed by the caller afterward. Immutability also makes exceptions safe to share across threads. Rule of thumb: all-final fields, defensive copies, no setters.

Throwable is the root of everything thrown; extending it directly creates a type that is neither a clean Exception nor an Error, confusing callers and catch blocks. Error is reserved for serious JVM-level failures (e.g. OutOfMemoryError) that applications are not meant to catch.

class MyFailure extends Error { }   // WRONG — implies an unrecoverable JVM error

class MyFailure extends RuntimeException { }  // right level of abstraction

Throwing an Error subtype tricks catch-all catch (Exception e) handlers into not catching it, and signals "the JVM is broken" when it isn't. Rule of thumb: subclass Exception or RuntimeException — leave Throwable and Error to the platform.

When a standard JDK exception already says exactly what's wrong, reuse it rather than inventing a parallel type. A bad argument is IllegalArgumentException; a bad object state is IllegalStateException; a null you reject is NullPointerException (or Objects.requireNonNull).

// no need for a custom "BadAgeException"
if (age < 0) throw new IllegalArgumentException("age must be >= 0");

Objects.requireNonNull(name, "name");  // throws NPE with a clear message

A codebase littered with one-off exception classes that nobody catches specifically is just noise. Rule of thumb: create a custom exception only when callers will catch it by type or need data from it; otherwise reuse a built-in.

Yes — throwing from a constructor is the correct way to reject invalid construction so no half-initialized object escapes. If the constructor throws, the object is never assigned to the variable and becomes garbage.

public class Percentage {
  private final int value;
  public Percentage(int value) {
    if (value < 0 || value > 100)
      throw new IllegalArgumentException("0..100, got " + value);
    this.value = value;   // only reached if valid
  }
}

Validate before assigning fields, and prefer unchecked exceptions here since invalid arguments are programming errors. Beware throwing from a constructor that already opened resources — the object's close() won't be called, so clean up first. Rule of thumb: fail fast in the constructor to enforce invariants.

Rethrowing is catching an exception, doing some work (log, clean up, add context), then throwing it onward so an outer handler still sees it. You can rethrow the same instance or wrap it.

try {
  process(record);
} catch (ServiceException e) {
  metrics.increment("process.failure");
  throw e;            // rethrow the SAME exception, trace intact
}

Rethrowing the same instance preserves the original stack trace perfectly. Since Java 7, precise rethrow lets the compiler narrow what a rethrown Exception can actually be, so you can declare specific subtypes in throws. Rule of thumb: rethrow the same object to keep the trace; wrap only when you need to change the abstraction level.

Exception translation is catching a low-level exception and throwing a higher-level one appropriate to your API's abstraction — so callers depend on your exception types, not on implementation details like SQLException or IOException.

// the repository hides JDBC behind its own abstraction
public User findById(long id) {
  try {
    return jdbc.queryForObject(SQL, mapper, id);
  } catch (SQLException e) {
    throw new UserRepositoryException("findById " + id, e); // translate + chain
  }
}

This keeps callers decoupled from the data layer; you could swap JDBC for an ORM without changing the exceptions they catch. Always chain the cause so the low-level detail is still available for debugging. Rule of thumb: translate at layer boundaries, and never let leaky low-level exceptions cross your API.

A business (domain) exception represents a violation of business rules that the application expects and often handles (InsufficientFundsException, DuplicateEmailException). A technical (system) exception represents infrastructure failure (DatabaseUnavailableException, network timeouts) that's usually unexpected and bubbles up to generic handling.

// business: expected, often caught and shown to the user
throw new InsufficientFundsException(accountId, amount);

// technical: infrastructure broke — log, alert, return 500
throw new ServiceUnavailableException("payment gateway down", cause);

Separating them lets you handle each differently: business errors map to friendly user messages and 4xx responses, technical ones to retries, alerts, and 5xx. Rule of thumb: model business failures as their own hierarchy, distinct from technical/infrastructure failures.

Modern style (popularized by Spring and much of the ecosystem) favors unchecked exceptions because they keep method signatures clean, work with lambdas and streams, and avoid forcing every caller to catch or re-declare. Handling is centralized instead of scattered.

// a single global handler turns domain exceptions into HTTP responses
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<?> handle(OrderNotFoundException e) {
  return ResponseEntity.status(404).body(e.getMessage());
}

The trade-off is the compiler no longer reminds callers an error can occur, so teams rely on documentation (@throws Javadoc) and convention instead. Some argue this loses checked exceptions' safety, but most accept it for the reduced ceremony. Rule of thumb: default to unchecked domain exceptions and handle them centrally, documenting them clearly.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel