Topic 7.5
Writing Your Own Exceptions
In one line
A custom exception is a class you write that extends Exception (checked) or RuntimeException (unchecked). It gives a failure a meaningful name, can carry extra data, and should keep the original exception as its cause.
Think of it like this
A hospital's alarm codes. 'Something is wrong' isn't helpful, so hospitals use specific codes: one means a fire, another means a patient's heart has stopped. Each code tells the right team exactly what to do, and the announcement includes details like the room number. A custom exception is your program's specific alarm code: a precise name, the details needed to act, and a note of what originally triggered it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Custom exception
- An exception class you write yourself, by extending
ExceptionorRuntimeException. - Cause
- The original exception that led to this one, stored inside it and returned by
getCause(). - Exception chaining
- Wrapping a caught exception as the cause of a new one, so the full story of the failure is kept.
- Exception translation
- Catching a low-level exception (like
SQLException) at a layer boundary and throwing one that fits the higher level (likeOrderStorageException). - Root cause
- The very first exception in a chain: the last
Caused by:in a stack trace. Usually the real problem. - `super(...)`
- A call, as the first line of a constructor, that runs the parent class's constructor with the given arguments (Phase 5).
- Error code
- A short, stable identifier such as
OUT_OF_STOCKstored in an exception, so code and support teams can recognise the problem without reading the message text. - `serialVersionUID`
- A version number for a class's serialized form.
Throwableis serializable, so exceptions sent between JVMs should declare one.
Step by step
01The smallest useful custom exception
Choose a name that describes the failure, choose checked or unchecked by picking the superclass, and pass the message to super.
That's all it takes to be throwable and catchable. Everything else (getMessage, stack trace, cause) is inherited from Throwable.
class OrderNotFoundException extends RuntimeException {
OrderNotFoundException(String orderId) {
super("no order with id " + orderId);
}
}
// somewhere in a service:
throw new OrderNotFoundException("A-1042");02The full template
For a reusable exception, provide the standard constructors so callers can always attach a cause. Keep any fields final and add no setters.
serialVersionUID matters only if exceptions are serialized (RMI, some distributed caches, old remoting frameworks). Declaring it costs one line and avoids InvalidClassException when two sides compile different versions.
public class PaymentException extends Exception { // checked
private static final long serialVersionUID = 1L;
public PaymentException(String message) {
super(message);
}
public PaymentException(String message, Throwable cause) {
super(message, cause);
}
public PaymentException(Throwable cause) {
super(cause); // message becomes cause.toString()
}
}03Keep the cause: chaining
When you catch something low-level and throw something higher-level, always pass the caught exception along. Without it, the log says 'cannot load config' and nobody knows whether the file was missing, unreadable or malformed.
The printed trace shows the new exception first, then Caused by: with the original. ... 1 more means the remaining frames are identical to the frames above, so they're not repeated.
try {
text = readFile(file);
} catch (IOException e) {
throw new ConfigException("cannot load config from " + file, e); // e is the cause
}04Carry data, not just text
If a handler would need to parse the message to get a number back out, put the number in a field instead. Messages are for humans; fields are for code.
Here a handler can show shortfall() to the user or decide whether to offer an overdraft, without string parsing.
class InsufficientFundsException extends RuntimeException {
private final long balance;
private final long requested;
InsufficientFundsException(long balance, long requested) {
super("balance " + balance + " is less than requested " + requested);
this.balance = balance;
this.requested = requested;
}
long shortfall() { return requested - balance; }
}05A small hierarchy for one domain
A base exception for a module lets callers catch 'anything that went wrong in the shop' in one place, while specific subclasses can be handled specially where it matters.
Keep it shallow: a base type and a handful of subtypes that callers actually distinguish. Deep trees of exception classes nobody catches separately are maintenance without benefit.
06Translate at layer boundaries
A web controller shouldn't need to know that orders are stored in PostgreSQL. If the repository lets SQLException escape, every caller depends on that detail. Translating keeps each layer's exceptions in its own vocabulary, and chaining keeps the details for debugging.
This is the same idea as Spring's DataAccessException hierarchy, which translates vendor-specific SQL errors into consistent unchecked exceptions (see the System Design course for layered architecture).
07Rules the compiler enforces
A custom checked exception follows catch-or-declare like any other. A generic class can't extend Throwable (class Failure<T> extends Exception is an error), because the JVM's catch matching works on erased run-time classes, so catch (Failure<String> e) could never be checked.
Constructors aren't inherited: if you only write (String message), then new MyException("x", e) doesn't compile. Add the constructors your callers need.
Try it yourself
- 1
Add a field to the hierarchy
Give
OutOfStockExceptiona fielditemwith an accessor, and in the handler printtry " + e.item() + " later. Predict the new output line, then run. - 2
Remove the cause
In the chaining example, change
new ConfigException("cannot load config from " + file, e)to drope. Predict whichcaused byline disappears. Imagine reading only that output in a production log at 3 a.m. - 3
Make it checked
Change
ShopExceptionto extendExceptioninstead ofRuntimeException. Predict where the compiler now complains (hint:orderand its throws). Fix it by addingthrows ShopExceptionand run again.
Code & diagrams
Expected output
error: balance 200 is less than requested 450
you need 250 more
is RuntimeException? true
balance: 200The for loop walks the cause chain down to the root cause, as a logging library does.
Expected output
cannot load config from missing.properties
caused by IOException: file not found: missing.properties
bad port '80x' in app.properties
caused by NumberFormatException: For input string: "80x"Expected output
ordered tea
suggest something else: mango is out of stock
[PAYMENT_DECLINED] payment declined: card blocked// BAD: the IOException (and its message and trace) is gone forever
catch (IOException e) {
throw new ConfigException("cannot load config");
}
// BAD: the message survives, the type and stack trace don't
catch (IOException e) {
throw new ConfigException("cannot load config: " + e.getMessage());
}
// GOOD: full chain preserved, printed as "Caused by: java.io.IOException: ..."
catch (IOException e) {
throw new ConfigException("cannot load config from " + file, e);
}
// For an exception class without a cause constructor (rare, old APIs):
ConfigException ce = new ConfigException("cannot load config");
ce.initCause(e); // allowed once, only if no cause was set yet
throw ce;Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Throw a custom checked exception without declaring it
With class AppException extends Exception, write static void run() { throw new AppException("x"); }.
Break #2
Make an exception generic
Declare class Failure<T> extends Exception { } to carry a typed payload.
Myth vs fact
Myth
Every error deserves its own exception class.
Fact
Only when a caller will catch it specifically or it carries data. Otherwise a standard IllegalArgumentException or IllegalStateException with a good message is clearer and costs nothing to maintain.
Myth
Putting the cause's message in the new message is enough.
Fact
That keeps the text but loses the cause's type, its stack trace and any deeper causes. Pass the exception itself as the cause.
Myth
Custom exceptions should be checked so callers can't ignore them.
Fact
Choose by whether the caller can recover. Many teams make most domain exceptions unchecked and handle them at a boundary (a web layer's exception handler), keeping checked exceptions for truly recoverable cases.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Throwable.getMessage()returns the detail message passed to the constructor;getLocalizedMessage()defaults to it and can be overridden for translations;toString()isgetClass().getName() + ": " + getLocalizedMessage(). TheThrowable(Throwable cause)constructor sets the message tocause.toString(), which is why wrapped exceptions show the inner class name in their message. - ▸
initCausecan be called only once and only if the cause wasn't set by a constructor (otherwise it throws anIllegalStateExceptionwhose message startsCan't overwrite cause). Passing the exception itself throwsIllegalArgumentException: Self-causation not permitted. Cycles longer than one are possible andprintStackTracedetects them, printing[CIRCULAR REFERENCE: ...]. - ▸
Domain exceptions that carry fields are often mapped to API responses in one place, for example a Spring
@ControllerAdvicethat turnsOutOfStockExceptioninto HTTP 409 with{"code": "OUT_OF_STOCK"}. Stable codes, not messages, are what clients should depend on. - ▸
For exceptions used purely for control flow inside a library (rare, but parsers do it), the protected
Throwable(String, Throwable, boolean enableSuppression, boolean writableStackTrace)constructor (Java 7) makes a stackless, cheap exception. Never expose such exceptions to users: they're undebuggable by design.
Remember this
- 1
Creating one is ordinary inheritance (Phase 5):
class OrderNotFoundException extends RuntimeException { ... }. The superclass decides the category: extendException(or another checked type such asIOException) for a checked exception, orRuntimeException(or a subclass such asIllegalStateException) for an unchecked one. Never extendThrowableorErrordirectly;Errormeans the JVM is failing. - 2
Give it the constructors callers need, passing values up with
super(...).ExceptionandRuntimeExceptionoffer four public constructors:(),(String message),(String message, Throwable cause)and(Throwable cause). Constructors aren't inherited, so your class has only the ones you write. At minimum, provide(String message)and(String message, Throwable cause). - 3
Exception chaining (Java 1.4) is the key habit. When you catch a low-level exception and throw a higher-level one, pass the original as the cause:
throw new ConfigException("cannot load " + file, e);. The cause travels with the new exception (getCause()), and the stack trace prints aCaused by:section, so nobody loses the real reason. Dropping the cause is one of the most common and costly mistakes in Java code. - 4
Custom exceptions can carry fields: an error code, the order ID, the balance and the amount requested. Handlers can then make decisions from data rather than parsing message strings. Make the fields
finaland set them in the constructor; an exception is a report of something that already happened, so it shouldn't change. - 5
A small hierarchy works well for a domain: a base
ShopExceptionand subclassesOutOfStockException,PaymentDeclinedException. Callers can catch one specific case, or the base type for everything from your module. Combined with chaining, this is exception translation: a repository catchesSQLExceptionand throwsOrderStorageException, so callers don't depend on the fact that you use SQL. - 6
Create one only when it adds something: a caller will catch it specifically, or it carries data. For a plain bad argument,
IllegalArgumentExceptionalready says everything. Name custom exceptions with theExceptionsuffix, write a message that includes the relevant values, and remember thatThrowableimplementsSerializable(declare aserialVersionUIDif your exceptions cross process boundaries). A generic class can't extendThrowableat all, becausecatchcan't distinguish type arguments that are erased at run time.
Explain it without notes
How do you decide whether a custom exception should extend Exception or RuntimeException?
What is exception chaining, and why is it important?
What constructors should a custom exception provide, and why?
When is a custom exception worth creating, and when should you use a standard one?
What is exception translation, and where do you do it?
Practice
Write an unchecked InvalidPinException with a field attemptsLeft. A method checkPin(String pin) accepts "1234" and otherwise throws it with the attempts left (start at 3). Try "0000", "1111" and "1234", printing what happens.
Write a checked ParseException-style class CsvException with a line field. A method int sumColumn(String[] lines) parses the second comma-separated value of each line and wraps any NumberFormatException with the line number (starting at 1) as the cause. Run it on {"a,1", "b,2", "c,x"}.
Write a method Throwable rootCause(Throwable t) that follows getCause() to the end. Test it on a three-level chain and print the root's class and message.
Trade-offs
- ↔
Specific exception classes make handling precise and self-documenting, but every class is API surface to maintain; a base exception with an error-code field can replace a dozen tiny subclasses.
- ↔
Translating exceptions at each layer decouples callers from implementation details, but over-wrapping produces chains five levels deep that are hard to read; translate at real boundaries, not in every method.
- ↔
Rich fields help handlers decide, but exceptions are often logged or serialized; avoid putting large objects or sensitive data (passwords, card numbers) into exception fields and messages.
Done when you can
Done when you can write a checked and an unchecked custom exception with the standard constructors.
Done when you always pass the caught exception as the cause when wrapping.
Done when you can read a trace with several
Caused by:sections and find the root cause.Done when you can design a small exception hierarchy with a base type and an error code.
Done when you can decide between a custom exception and a standard one, and justify it.