Overview
In this article, we'll cover the process of creating custom both checked and unchecked exceptions in Java.
If you'd like to read more about exceptions and exception handling in Java, we've covered it in detail in - Exception Handling in Java: A Complete Guide with Best and Worst Practices
Why Use Custom Exceptions?
Although Java exceptions, as they are, cover nearly all exceptional cases and conditions, your application might throw a specific custom exception, unique to your code and logic.
Sometimes, we need to create our own for representing business logic exceptions, i.e. exceptions that are specific to our business logic or workflow. For example EmailNotUniqueException
, InvalidUserStateException
, etc.
They help application clients to better understand what went wrong. They are particularly useful for doing exception handling for REST APIs as different business logic constraints require different response codes to be sent back to the client.
If defining a custom exception doesn't provide benefit over using a regular exception in Java, there's no need to define custom exceptions and you should just stick to the ones that are already available - no need to invent hot water again.
Custom Checked Exception
Let's consider a scenario where we want to validate an email that is passed as an argument to a method.
We want to check whether it's valid or not. Now we could use Java's built-in IllegalArgumentException
, which is fine if we are just checking for a single thing like whether it matches a predefined REGEX or not.
But suppose we also have a business condition to check that all emails in our system need to be unique. Now we have to perform a second check (DB/network call). We can, of course, use the same IllegalArgumentException
, but it will be not clear what the exact cause is - whether the email failed REGEX validation or the email already exists in the database.
Let's create a custom exception to handle this situation. To create an exception, like any other exception, we have to extend the java.lang.Exception
class:
public class EmailNotUniqueException extends Exception {
public EmailNotUniqueException(String message) {
super(message);
}
}
Note that we provided a constructor that takes a String
error message and calls the parent class constructor. Now, this is not mandatory but it's a common practice to have some sort of details about the exception that occurred.
By calling super(message)
, we initialize the exception's error message and the base class takes care of setting up the custom message, according to the message
.
Now, let's use this custom exception in our code. Since we're defining a method that can throw an exception in the service layer, we'll mark it with the throws
keyword.
If the input email already exists in our database (in this case a list), we throw
our custom exception:
public class RegistrationService {
List<String> registeredEmails = Arrays.asList("[email protected]", "[email protected]");
public void validateEmail(String email) throws EmailNotUniqueException {
if (registeredEmails.contains(email)) {
throw new EmailNotUniqueException("Email Already Registered");
}
}
}
Now let's write a client for our service. Since it's a checked exception, we have to adhere to the handle-or-declare rule. In the proceeding example, we decided to "handle" it:
public class RegistrationServiceClient {
public static void main(String[] args) {
RegistrationService service = new RegistrationService();
try {
service.validateEmail("[email protected]");
} catch (EmailNotUniqueException e) {
// logging and handling the situation
}
}
}
Running this piece of code will yield:
mynotes.custom.checked.exception.EmailNotUniqueException: Email Already Registered
at mynotes.custom.checked.exception.RegistrationService.validateEmail(RegistrationService.java:12)
at mynotes.custom.checked.exception.RegistrationServiceClient.main(RegistrationServiceClient.java:9)
Note: The exception handling process is omitted for brevity but is an important process nonetheless.
Custom Unchecked Exception
This works perfectly fine but our code has become a bit messy. Additionally, we are forcing the client to capture our exception in a try-catch
block. In some cases, this can lead to forcing developers to write boilerplate code.
In this case, it can be beneficial to create a custom runtime exception instead. To create a custom unchecked exception we have to extend the java.lang.RuntimeException
class.
Let's consider the situation where we have to check if the email has a valid domain name or not:
public class DomainNotValidException extends RuntimeException {
public DomainNotValidException(String message) {
super(message);
}
}
Now let use it in our service:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
public class RegistrationService {
public void validateEmail(String email) {
if (!isDomainValid(email)) {
throw new DomainNotValidException("Invalid domain");
}
}
private boolean isDomainValid(String email) {
List<String> validDomains = Arrays.asList("gmail.com", "yahoo.com", "outlook.com");
if (validDomains.contains(email.substring(email.indexOf("@") + 1))) {
return true;
}
return false;
}
}
Notice that we did not have to use the throws
keywords at the method signature since it is an unchecked exception.
Now let's write a client for our service. We don't have to use a try-catch
block this time:
public class RegistrationServiceClient {
public static void main(String[] args) {
RegistrationService service = new RegistrationService();
service.validateEmail("[email protected]");
}
}
Running this piece of code will yield:
Exception in thread "main" mynotes.custom.unchecked.exception.DomainNotValidException: Invalid domain
at mynotes.custom.unchecked.exception.RegistrationService.validateEmail(RegistrationService.java:10)
at mynotes.custom.unchecked.exception.RegistrationServiceClient.main(RegistrationServiceClient.java:7)
Note: Of course, you can surround your code with a try-catch
block to capture the arising exception but now it's not being forced by the compiler.
Rethrowing an Exception Wrapped Inside a Custom Exception
Sometimes we need to catch an exception and re-throw it by adding in a few more details. This is usually common if you have various error codes defined in your application that needs to be either logged or returned to the client in case of that particular exception.
Suppose your application has a standard ErrorCodes
class:
public enum ErrorCodes {
VALIDATION_PARSE_ERROR(422);
private int code;
ErrorCodes(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
Let's create our custom exception:
public class InvalidCurrencyDataException extends RuntimeException {
private Integer errorCode;
public InvalidCurrencyDataException(String message) {
super(message);
}
public InvalidCurrencyDataException(String message, Throwable cause) {
super(message, cause);
}
public InvalidCurrencyDataException(String message, Throwable cause, ErrorCodes errorCode) {
super(message, cause);
this.errorCode = errorCode.getCode();
}
public Integer getErrorCode() {
return errorCode;
}
}
Notice we have multiple constructors and let the service class decide which one to use. Since we are rethrowing the exception, it's always a good practice to capture the root cause of the exception hence the Throwable
argument which can be passed` to the parent class constructor.
We are also capturing the error code in one of the constructors and set the errorCode
within the exception itself. The errorCode
could be used by the client for logging or any other purpose. This helps in a more centralized standard for exception handling.
Lets write our service class:
public class CurrencyService {
public String convertDollarsToEuros(String value) {
try {
int x = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new InvalidCurrencyDataException("Invalid data", e, ErrorCodes.VALIDATION_PARSE_ERROR);
}
return value;
}
}
So we caught the standard NumberFormatException
and threw our own InvalidCurrencyDataException
. We passed the parent exception to our custom exception so that we won't lose the root cause from which they occurred.
Let's write a test client for this service:
public class CurrencyClient {
public static void main(String[] args) {
CurrencyService service = new CurrencyService();
service.convertDollarsToEuros("asd");
}
}
Output:
Exception in thread "main" mynotes.custom.unchecked.exception.InvalidCurrencyDataException: Invalid data
at mynotes.custom.unchecked.exception.CurrencyService.convertDollarsToEuro(CurrencyService.java:10)
at mynotes.custom.unchecked.exception.CurrencyClient.main(CurrencyClient.java:8)
Caused by: java.lang.NumberFormatException: For input string: "asd"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at mynotes.custom.unchecked.exception.CurrencyService.convertDollarsToEuro(CurrencyService.java:8)
... 1 more
As you can see we have a nice stack-trace of the exception which could be useful for debugging purposes.
Best Practices for Custom Exceptions
- Adhere to the general naming convention throughout the Java ecosystem - All custom exception class names should end with "Exception"
- Avoid making custom exceptions if standard exceptions from the JDK itself can serve the purpose. In most cases, there's no need to define custom exceptions.
- Prefer runtime exceptions over checked exceptions. Frameworks like Spring have wrapped all checked exception to runtime exceptions, hence not forcing the client to write boilerplate code that they don't want or need to.
- Provide many overloaded constructors based on how the custom exception would be thrown. If it's being used for rethrowing an existing exception then definitely provide a constructor that sets the cause.
Conclusion
Custom exceptions are used for specific business logic and requirements. In this article, we've discussed the need for them as well as covered their usage.
The code for the examples used in this article can be found on Github.