How to Create a Custom Exception in Java
The exception class compiles, it is thrown and caught — and e.getMessage() still prints null. This happens when you store the error text in a field of your own and forget to call super(message):
public class ValidationException extends Exception {
private String message;
public ValidationException(String message) {
this.message = message; // super(message) is not called
}
}
// somewhere in the code:
try {
throw new ValidationException("Email must not be empty");
} catch (ValidationException e) {
System.out.println(e.getMessage()); // Output: null
} A custom (user-defined) exception in Java is a class that extends Exception (a checked exception) or RuntimeException (an unchecked exception). To create your own exception, declare a class whose name ends with Exception, choose the parent class and add constructors that pass the message and the cause to super(...). Everything else — getMessage(), getCause(), printStackTrace() — the class inherits.
How to create a custom exception in Java
To create a custom exception class, declare a class that extends Exception or RuntimeException and add a constructor that calls super(message). You do not have to implement any methods: a minimal user-defined exception consists of the class declaration and a single constructor.
public class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
} Such an exception is thrown with the throw statement, and because it extends Exception, the method must declare it in its throws clause (both keywords are covered in separate lessons of this module):
public class AgeCheck {
static void checkAge(int age) throws InvalidAgeException {
if (age < 0 || age > 150) {
throw new InvalidAgeException("Invalid age: " + age);
}
System.out.println("Age accepted: " + age);
}
public static void main(String[] args) {
try {
checkAge(25);
checkAge(-3);
} catch (InvalidAgeException e) {
System.out.println("Error: " + e.getMessage());
}
}
} Output:
Age accepted: 25
Error: Invalid age: -3 Where the methods come from
The Exception class itself declares only constructors. The methods getMessage(), getCause(), getStackTrace(), printStackTrace() and toString() are inherited from Throwable — that is why your subclass gets them without writing a single line. The full list is in the section Methods of the Throwable class.
Extends Exception vs extends RuntimeException
Extend Exception when the calling code can and should handle the situation (the compiler will force it to), and extend RuntimeException when the exception signals a programming error or a broken method contract. The parent class decides whether your custom exception is checked or unchecked.
| Parent class | Exception kind | throws / try-catch required? | When to choose it |
|---|---|---|---|
extends Exception | Checked | Yes, otherwise the compile-time error unreported exception ... must be caught or declared to be thrown | The situation is expected and recoverable: a payment is declined, a file is not found, a password is wrong |
extends RuntimeException | Unchecked | No | A broken contract or a logic error: an invalid argument, an illegal object state |
extends Error | Unchecked | No | Do not use it: Error is reserved for JVM-level problems (OutOfMemoryError, StackOverflowError) |
extends Throwable | Checked | Yes | Do not use it: such an exception is not caught by the usual catch (Exception e) |
Which JDK classes are checked and which are unchecked is explained in the lesson Checked vs unchecked exceptions in Java.
Rule of thumb
Ask yourself: «What can the calling code reasonably do after catching this exception?» If there is a meaningful reaction (retry the request, show the user a message, take another path), make the exception checked. If the only option is to fix the code, choose RuntimeException. Modern frameworks such as Spring and Hibernate make almost all of their exceptions unchecked.
Exception constructors: message and cause
Constructors are not inherited, so every variant you need must be declared in your class and passed on to super(...). Two are usually enough: one with a message (String message) and one with a message and a cause (String message, Throwable cause) — the second keeps the original exception when you wrap it.
The Exception class (and likewise RuntimeException) provides five constructors that you can call through super:
| Constructor | What it stores | When to use it |
|---|---|---|
Exception() | Nothing: getMessage() returns null | Rarely: when the exception type alone says everything |
Exception(String message) | The error text | The main case |
Exception(String message, Throwable cause) | The text and the original exception | Wrapping a low-level exception (SQL, IO, parsing) into your own |
Exception(Throwable cause) | The cause; the message is cause.toString() | When there is nothing to add to the cause |
protected Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) | All of the above plus two flags (Java 7+) | Special cases: an exception without a stack trace or without suppressed exceptions |
Here is an exception class with two constructors and exception chaining: a NumberFormatException is wrapped into a custom exception, and the original cause stays available through getCause().
public class OrderProcessingException extends Exception {
public OrderProcessingException(String message) {
super(message);
}
public OrderProcessingException(String message, Throwable cause) {
super(message, cause);
}
} public class QuantityParser {
static int parseQuantity(String input) throws OrderProcessingException {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new OrderProcessingException("Invalid item quantity: " + input, e);
}
}
public static void main(String[] args) {
try {
parseQuantity("12a");
} catch (OrderProcessingException e) {
System.out.println(e.getMessage());
System.out.println("Cause: " + e.getCause());
}
}
} Output:
Invalid item quantity: 12a
Cause: java.lang.NumberFormatException: For input string: "12a" Without passing e to the constructor, the second line would print Cause: null, and the Caused by: java.lang.NumberFormatException block would disappear from the stack trace — while debugging you would have to guess what actually went wrong.
A custom exception with extra fields and toString()
A custom exception can carry fields with data about the error — an error code, the invalid value, an identifier — together with getters. The handler then receives structured information, not just text. Override toString() when you need your own format for printing the exception to a log or the console.
public class MyException extends Exception {
private final int detail;
public MyException(int detail, String message) {
super(message);
this.detail = detail;
}
public int getDetail() {
return detail;
}
@Override
public String toString() {
return "MyException{"
+ "detail=" + detail
+ ", message=" + getMessage()
+ "}";
}
} public class MyExceptionExample {
public static void main(String[] args) {
try {
compute(1);
compute(20);
} catch (MyException e) {
System.out.println("Caught: " + e);
}
}
public static void compute(int a) throws MyException {
System.out.println("compute(" + a + ") called");
if (a > 10) {
throw new MyException(a, "Value is greater than 10");
}
System.out.println("Normal exit.");
}
} Output:
compute(1) called
Normal exit.
compute(20) called
Caught: MyException{detail=20, message=Value is greater than 10} The string concatenation "..." + e calls toString(). Without the override, the implementation from Throwable would return «fully qualified class name: message», and the last line would be Caught: MyException: Value is greater than 10 (with the package name for a class in a package). The detail field would not appear there, but it would still be available through e.getDetail().
Custom unchecked exception example: extends RuntimeException
A custom unchecked exception is created the same way, only the parent class is RuntimeException. You do not have to declare it in throws or wrap the call in try-catch: the compiler does not check it, so you catch the exception only where it actually makes sense.
public class InsufficientFundsException extends RuntimeException {
private final long shortage;
public InsufficientFundsException(long shortage) {
super("Insufficient funds, short by: " + shortage);
this.shortage = shortage;
}
public long getShortage() {
return shortage;
}
} public class Account {
private long balance = 100;
public void withdraw(long amount) { // no throws needed
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
}
public static void main(String[] args) {
Account account = new Account();
account.withdraw(30); // balance is 70
try {
account.withdraw(500);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
System.out.println("Amount to top up: " + e.getShortage());
}
}
} Output:
Insufficient funds, short by: 430
Amount to top up: 430 Remove the try-catch, and the program still compiles, but it terminates with Exception in thread "main" InsufficientFundsException: Insufficient funds, short by: 430 followed by the stack trace. With a checked exception, the same code without try-catch or throws would not compile at all.
When to create a custom exception and when to reuse a standard one
Create your own exception when the error has a meaning in your business domain (OrderNotFoundException, WrongPasswordException), when you need to pass extra data, or when the calling code must catch exactly this error separately from others. For typical contract violations, reuse a standard JDK exception — every Java developer knows what it means.
| Situation | Standard exception |
|---|---|
| A method argument has an invalid value | IllegalArgumentException |
An argument is null although it must not be | NullPointerException (via Objects.requireNonNull(arg, "...")) |
| A method is called while the object is in the wrong state (for example, the connection is already closed) | IllegalStateException |
| An index is out of the valid range | IndexOutOfBoundsException (via Objects.checkIndex(index, length), Java 9+) |
| The operation is not supported by the implementation | UnsupportedOperationException |
A typical use case for a custom exception is the boundary between application layers: the data access layer catches SQLException and throws its own DataAccessException with the cause, so the business logic does not depend on JDBC details. When you have many exceptions, give them a common parent (for example, ShopException and its subclasses) — then one catch handles the whole group. Remember that a catch for a subclass must come before a catch for its parent, otherwise the code does not compile: see Catching Subclasses: Order of Catch Blocks.
Where developers get tripped up
Most problems with custom exceptions are not about syntax but about lost information: an empty message, a lost cause, a badly chosen parent class. The compiler does not catch these mistakes — they show up only when you are already debugging.
- A
messagefield of your own instead ofsuper(message). As in the example at the top of the lesson,getMessage()and the stack trace shownull. The fix: delete the field and callsuper(message)in the constructor. - Losing the cause.
throw new MyException("Database error")insidecatch (SQLException e)throws the original exception away. Writethrow new MyException("Database error", e)and declare a constructor with aThrowable causeparameter. - Extending
ThrowableorError. The first is not caught bycatch (Exception e); the second is misleading, becauseErrormeans a JVM failure, not an application error. - A name without the
Exceptionsuffix. A class namedWrongPasswordis formally legal, but by convention exception names end withException— that makes them easy to spot in code and logs. - The
serialVersionUIDwarning.ThrowableimplementsSerializable, so IDEs and the compiler with the-Xlint:serialflag warn that the field is missing. It is not an error; to silence the warning, addprivate static final long serialVersionUID = 1L;. - Overriding rules for
throws. An overriding method cannot declare broader checked exceptions than the parent method, and that applies to your own exception classes too. These rules are covered in a separate lesson of this module on exceptions and inheritance.
Frequently Asked Questions
Can a custom exception be a generic class or a record?
No. A subclass of Throwable cannot have type parameters: class MyException<T> extends Exception fails with the compile-time error a generic class may not extend java.lang.Throwable, because after type erasure a catch block could not tell MyException<String> from MyException<Integer>. A record does not work either: it implicitly extends java.lang.Record and cannot extend another class.
Do I need a serialVersionUID in an exception class?
It is not required: the code compiles and runs without it. But Throwable implements Serializable, and if exceptions travel between JVMs (RMI, distributed caches) or are serialized to files, an explicit serialVersionUID protects you from InvalidClassException after the class changes. As a bonus, the IDE warning disappears.
How do I create an exception without a stack trace to make it faster?
Call the protected constructor added in Java 7: super(message, null, false, false). The last parameter, writableStackTrace = false, turns off filling in the stack trace, which makes creating the exception noticeably cheaper. The price is debugging: the log no longer shows where the exception happened, so the trick is justified only for exceptions that are thrown very often and are always handled.
Can I declare an exception class inside another class?
Yes. Usually it is a static nested class, for example public static class NotFoundException extends RuntimeException inside the service it belongs to; from outside it is referenced as UserService.NotFoundException. A non-static inner class is also legal if the outer class is not generic, but its instances are tied to an instance of the outer class, so it is almost never used for exceptions.
Practice
Reinforce this lesson with hands-on tasks, each with a level and a time estimate.
All module tasks →
Comments