Exceptions refer to various unexpected situations, such as: file cannot be found, network connection failure, illegal parameters, etc.
An exception is an event that occurs during program execution and interferes with the normal flow of instructions.
What You Need
- About 8 minutes
- A favorite text editor or IDE
- Java 8 or later
- 3. Exception Best Practice
- 3.1 Don’t use exceptions to control program flow
- 3.2 Clean up resources in a finally block or use a try-with-resource statement
- 3.3 Use standard exceptions whenever possible
- 3.4 Document exceptions
- 3.5 Catch the most specific exceptions first
- 3.6 Don't catch Throwable class
- 3.7 Don't ignore exceptions
- 3.8 Don't log and throw exceptions
- 3.9 Don’t throw away the original exception when wrapping it
- 3.10 Don't use return in finally block
3. Exception Best Practice
Handling exceptions in Java is not a simple matter.
It is not only difficult for beginners to understand, but even some experienced developers need to spend a lot of time thinking about how to handle exceptions, including which exceptions need to be handled, how to handle them etc.
This is why most development teams will set some rules to standardize exception handling.
Here are a few exception handling best practices used by many teams.
3.1 Don’t use exceptions to control program flow
Exceptions should only be used for abnormal situations, they should never be used for normal control flow.
In other words, exceptions that can be avoided through pre-checking should not be handled through try-catch.
For instance, it is better to check if an object is null instead of catch a null pointer exception.
Rather :
if(obj != null) {
obj.method();
}
Than :
try {
obj.method();
} catch(NullPointerException e) {
...
}
The reason consists of the fact that the exception mechanism is originally designed to be used in abnormal situations.
Therefore, creating, throwing, and catching exceptions is expensive operations which impact performance of JVM.
Below code snippet shows the time-consuming comparison of creating objects, creating exception objects, and throwing and catching exception objects.
public class ExceptionTest19 {
private static void newObject(int n) {
long l = System.nanoTime();
for (int i = 0; i < n; i++) {
new Object();
}
System.out.println("Create Objects : " + (System.nanoTime() - l));
}
private static void newException(int n) {
long l = System.nanoTime();
for (int i = 0; i < n; i++) {
new Exception();
}
System.out.println("Create Exception Objects : " + (System.nanoTime() - l));
}
private static void catchException(int n) {
long l = System.nanoTime();
for (int i = 0; i < n; i++) {
try {
throw new Exception();
} catch (Exception e) {
}
}
System.out.println("Create Throw and Catch Exception Objects : " + (System.nanoTime() - l));
}
public static void main(String[] args) {
int n = 10000;
newObject(n);
newException(n);
catchException(n);
}
}
The output of above code snippet is below :
Create Objects : 329779
Create Exception Objects : 19213926
Create Throw and Catch Exception Objects : 16820043
3.2 Clean up resources in a finally block or use a try-with-resource statement
When using a resource like an InputStream that needs to be closed after use, a common mistake is to close the resource at the end of the try block.
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
This code can only work normally if no exception is thrown.
The code within the try block will execute normally and the resource can be closed normally.
But if one or more methods throw an exception before executing to the end of the try block, the resource will not be closed.
The right way to close a resource are one of the followings :
- Use finally block
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
- Use try-with-resource
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
...
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
3.3 Use standard exceptions whenever possible
Code reuse is a general rule, and it is the same for exceptions.
Reusing existing exceptions makes programs more readable because they are not filled with exceptions that are unfamiliar to the programmer.
There are several Java standard exceptions that are frequently used :
EXCEPTION | WHEN TO USE |
---|---|
IllegalArgumentException | Thrown to indicate that a method has been passed an illegal or inappropriate argument. |
IllegalStateException | Signals that a method has been invoked at an illegal or inappropriate time. |
NullPointerException | Thrown when an application attempts to use null in a case where an object is required. |
IndexOutOfBoundsException | Thrown to indicate that an index of some sort (such as to an array, to a string, or to a vector) is out of range. |
ConcurrentModificationException | This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible. |
UnsupportedOperationException | Thrown to indicate that the requested operation is not supported. |
3.4 Document exceptions
When an exception is declared on a method, it also needs to be documented.
The purpose is to provide the caller with as much information as possible so that exceptions can be better avoided or handled.
Add the @throws statement in Javadoc to describe the exception and related information as accurately as possible, so that whether it is printed to the log or in the monitoring tool, it can be read more easily, specific error messages and errors can be better located.
/**
*
* Method description
*
* @throws MyBusinessException - businuess exception description
*
*/
public void doSomething(String input) throws MyBusinessException {
// ...
}
3.5 Catch the most specific exceptions first
Always catch the most specific exception class first and add less specific catch blocks to the end of the list.
public class ExceptionTest13 {
public static void main(String[] args) {
try {
System.out.println(Integer.parseInt("123 "));
} catch (NumberFormatException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
}
The output of above code snippet is below :
java.lang.NumberFormatException: For input string: "123 "
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at java.base/java.lang.Integer.parseInt(Integer.java:778)
at ExceptionTest13.main(ExceptionTest13.java:4)
Most IDEs can help to implement this best practice.
When trying to catch less specific exceptions first, they report an unreachable block of code.
In above code snippet, if we catch firstly IllegalArgumentException before NumberFormatException like below :
public class ExceptionTest13 {
public static void main(String[] args) {
try {
System.out.println(Integer.parseInt("123 "));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
Visual Studio Code will display below message :
Unreachable catch block for NumberFormatException. It is already handled by the catch block for IllegalArgumentExceptionJava(553648315)
java.lang.NumberFormatException
Thrown to indicate that the application has attempted to convert a string to one of the numeric types, but that the string does not have the appropriate format.
Since:
1.0
See Also:
java.lang.Integer.parseInt(String)
3.6 Don’t catch Throwable class
Throwable is the super class for all exceptions and errors.
It is possible to use it in a catch clause, but we should never do it !
If we use Throwable in a catch clause, it will catch not only all exceptions but also all errors.
The JVM throws errors indicating serious problems that should not be handled by the application.
Typical examples are OutOfMemoryError or StackOverflowError.
Both are caused by conditions outside the application’s control and cannot be handled.
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
3.7 Don’t ignore exceptions
Many times, developers are confident that an exception will not be thrown, so they write a catch block but do not do any processing or logging.
public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen
}
}
But the reality is that unpredictable exceptions often occur, at this time, because the exception is caught, it is impossible to get enough error information to positioning problem.
A reasonable approach is to at least log exception information.
public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e);
}
}
3.8 Don’t log and throw exceptions
Many codes and even class libraries will have logic to catch exceptions, log them, and throw them again.
This way of handling exception seems reasonable, but this often outputs multiple logs for the same exception.
public class ExceptionTest14 {
public static void main(String[] args) {
try {
Long.parseLong("xyz");
} catch (NumberFormatException e) {
System.err.println(e);
throw e;
}
}
}
The output of above code snippet is below :
java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Long.parseLong(Long.java:709)
at java.base/java.lang.Long.parseLong(Long.java:832)
at ExceptionTest14.main(ExceptionTest14.java:4)
So either log the exception or throw it, better not to do the two of them together.
3.9 Don’t throw away the original exception when wrapping it
It is a very common practice to catch standard exceptions and wrap them as custom exceptions.
This allows us to add more specific exception information and perform targeted exception handling.
When doing so, make sure to set the original exception as the reason.
The Exception class provides a special constructor that accepts a Throwable as parameter.
Otherwise, we will lose the stack trace and the original exception message, which will make it difficult to analyze the events that caused the exception.
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
3.10 Don’t use return in finally block
After the return statement in the try block is successfully executed, it does not return immediately, but continues to execute the statement in the finally block.
If there is a return statement in finally block, it returns directly, discarding the return in the try block.
public class ExceptionTest15 {
public static void main(String[] args) {
int x = checkReturn();
System.out.println("x = " + x);
}
static int checkReturn() {
int x = 0;
try {
// x = 1 but not return immediately
return ++x;
} finally {
// x = 2 and return immediately
return ++x;
}
}
}
The output of above code snippet is below :
x = 2