02 What's the Difference Between Exception and Error

02 What’s the difference between Exception and Error #

Are there programs in the world that will never fail? Perhaps this only happens in the dreams of programmers. With the birth of programming languages and software, exceptional situations have been entangled with us like shadows. Only by handling unexpected situations correctly can the reliability of a program be guaranteed.

Java, since its inception, has provided a relatively complete exception handling mechanism. This is one of the reasons why Java has become popular, as this mechanism greatly reduces the threshold for writing and maintaining reliable programs. Nowadays, the exception handling mechanism has become a standard feature of modern programming languages.

Today, I want to ask you a question: Please compare Exception and Error. In addition, what is the difference between runtime exceptions and general exceptions?

Typical Answer #

Exception and Error both inherit from the Throwable class. In Java, only instances of the Throwable type can be thrown or caught, making it the fundamental component of the exception handling mechanism.

Exception and Error reflect the classification of different exceptional situations by Java platform designers. Exception refers to unexpected situations that can occur during normal program execution and can and should be caught and handled.

Error refers to exceptional situations that are unlikely to occur under normal circumstances, and the majority of errors result in the program (such as the JVM itself) being in an abnormal and unrecoverable state. Since these are abnormal situations, they are not convenient or necessary to catch. Common examples include OutOfMemoryError and other subclasses of Error.

Exception can be further divided into checked exceptions and unchecked exceptions. Checked exceptions must be explicitly caught and handled in the source code, as this is part of the compilation-time check. The unchecked exceptions, like NullPointerException and ArrayIndexOutOfBoundsException, are typically logic errors that can be avoided through coding and do not require mandatory catching at compilation time.

Analysis of Key Points #

The analysis of the difference between Exception and Error is an examination of the Java processing mechanism from a conceptual perspective. Overall, it is still at the level of understanding, and as long as the interviewee can explain it clearly, it is sufficient.

In our daily programming, how to handle exceptions well is a test of our foundation. I think we need to master two aspects.

Firstly, understand the design and classification of Throwable, Exception, and Error. For example, master the most widely used subclasses and how to customize exceptions.

Many interviewers will further ask some details, such as, do you understand which Error, Exception, or RuntimeException? I have drawn a simple class diagram and listed some typical examples for you to refer to, at least to have a basic understanding.

Class Diagram

It is better to focus on understanding some of the subtypes, such as the difference between NoClassDefFoundError and ClassNotFoundException, which is also a classic introductory question.

Secondly, understand the elements and practices of handling Throwable in the Java language. It is necessary to master the most basic syntax, such as try-catch-finally blocks, throw and throws keywords, etc. At the same time, you should also know how to handle typical scenarios.

Exception handling code can be cumbersome, for example, we need to write a lot of repetitive capture code, or do some resource recovery work in finally. With the development of the Java language, some more convenient features have been introduced, such as try-with-resources and multiple catch, you can refer to the code snippet below. During compilation, the corresponding handling logic will be automatically generated, such as automatically closing objects that extend AutoCloseable or Closeable according to convention.

try (BufferedReader br = new BufferedReader();
     BufferedWriter writer = new BufferedWriter()) {// Try-with-resources
    // do something
catch ( IOException | XEception e) {// Multiple catch
   // Handle it
}

Knowledge Expansion #

Most of what was discussed before was conceptual. Now I will discuss some practical choices, and I will analyze them with the help of some code examples.

Let’s start with the first one. The code below reflects what is wrong in exception handling?

try {
  // Business logic
  // ...
  Thread.sleep(1000L);
} catch (Exception e) {
  // Ignore it
}

Although this code is short, it already violates two basic principles of exception handling.

Firstly, try to avoid catching general exceptions like Exception, but instead catch specific exceptions. In this case, it should catch InterruptedException thrown by Thread.sleep().

This is because in daily development and collaboration, we often have more opportunities to read code than to write code. Software engineering is an art of collaboration, so it is our obligation to make our code reflect as much information as possible in an intuitive way. Using generic exceptions like Exception instead actually hides our intentions. Furthermore, we should also ensure that the program does not catch exceptions that we do not want to catch. For example, you might prefer RuntimeException to be propagated rather than caught.

Moreover, unless deeply considered, avoid catching Throwable or Error as it is difficult to ensure proper handling of OutOfMemoryError.

Secondly, do not swallow exceptions. This is something to pay special attention to in exception handling, as it may lead to very difficult-to-diagnose erratic situations.

Swallowing exceptions is often based on assumptions that this piece of code may not occur or that ignoring the exception does not matter. However, do not make such assumptions in production code!

If we do not throw the exception or output it to a log (e.g. Logger), the program may end in an uncontrollable manner in the subsequent code. It’s impossible to easily determine where the exception is thrown and why.

Now let’s take a look at the second code snippet:

try {
   // Business logic
   // ...
} catch (IOException e) {
    e.printStackTrace();
}

This code is fine as an experimental code, but in production code, this is generally not allowed. Take a moment to think about why.

Let’s take a look at the documentation for printStackTrace() which states: “Prints this throwable and its backtrace to the standard error stream”. The problem lies here. In slightly more complex production systems, the standard error (STDERR) may not be an appropriate output option, as it is difficult to determine where the output actually goes.

Especially for distributed systems, if an exception occurs but the stack trace cannot be found, it becomes an obstacle to diagnosis. Therefore, it is best to use product logs to output detailed information to the logging system.

Let’s now take a look at the following code snippet to understand the principle of Throw early, catch late.

public void readPreferences(String fileName){
   //...perform operations... 
InputStream in = new FileInputStream(fileName);
// ...读取preferences文件...
}

If fileName is null, the program will throw a NullPointerException. However, since the problem is not immediately exposed, the stack trace can be very confusing and often requires complex debugging. This NPE is just an example, but in actual production code, there may be various scenarios, such as failed configuration retrieval. When encountering a problem, throwing it early can provide a clearer reflection of the problem.

We can modify it to “throw early” to make the corresponding exception message more intuitive.

public void readPreferences(String filename) {
  Objects.requireNonNull(filename);
  // ...perform other operations... 
  InputStream in = new FileInputStream(filename);
  // ...read the preferences file...
}

As for “catch late,” it is actually a problem that we often struggle with. After catching an exception, how should we handle it? The worst way to handle it is what I mentioned earlier, which is to “swallow the exception,” essentially hiding the problem. If you really don’t know how to handle it, you can choose to preserve the cause information of the original exception and either rethrow it directly or construct a new exception to throw. At a higher level, because we have clear (business) logic, we often know what the appropriate handling method is.

Sometimes, we may customize exceptions according to our needs. In addition to ensuring that enough information is provided, there are two points to consider:

  • Whether it needs to be defined as a Checked Exception, because the purpose of this type design is more for recovering from exceptional situations. As the designers of the exceptions, we often have sufficient information to categorize them.
  • While ensuring that the diagnostic information is sufficient, also consider avoiding including sensitive information, as that may lead to potential security issues. If we look at the Java standard library, you may notice examples like java.net.ConnectException, where the error message is something like “Connection refused (Connection refused)” and does not include specific machine names, IPs, ports, etc. One important consideration is information security. Similar situations also occur in logging, where user data generally should not be output to logs.

There is an ongoing debate (and even a degree of consensus) in the industry that Java’s Checked Exception may be a design mistake. Opponents have cited several points:

  • The assumption of Checked Exception is that we catch the exception and then recover the program. However, in most cases, we are simply not able to recover. The use of Checked Exception has greatly deviated from its original design purpose.
  • Checked Exception is incompatible with functional programming. If you have written Lambda/Stream code, you will likely have experienced this.

Many open-source projects have adopted this practice, such as Spring and Hibernate, and it is even reflected in the design of new programming languages like Scala. If you are interested, you can refer to:

http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/

Of course, many people think that there is no need to go to extremes because there are indeed some exceptions, such as those related to IO, networking, etc., which actually have recoverability, and Java has proven its ability to build high-quality software through massive industry practices. I will not further interpret it here. Interested students can click on the link to watch the video of Bruce Eckel’s presentation, “Failing at Failing: How and Why We’ve Been Nonchalantly Moving Away From Exception Handling” at the 2018 Global Software Development Conference QCon.

Let’s examine Java’s exception handling mechanism from a performance perspective. Here are two potentially expensive aspects:

  • The try-catch block incurs additional performance overhead, or, from another perspective, it often affects the JVM’s ability to optimize the code. Therefore, it is recommended to only catch the necessary code segments and avoid enclosing a large try block around the entire code. At the same time, using exceptions to control the code flow is not a good idea and is much less efficient than our usual conditional statements (if/else, switch).
  • For every instantiation of an Exception in Java, a snapshot of the stack at that time is taken, which is a relatively heavy operation. If it happens frequently, this overhead cannot be ignored.

Therefore, for low-level libraries that pursue extreme performance, one approach is to try creating an Exception that does not capture the stack trace. There is controversy in doing this because the assumption is that when I create the Exception, I know whether or not the stack trace will be needed in the future. The problem is, is that really possible? Perhaps on a small scale, but in large-scale projects, this may not be a rational choice. If a stack trace is needed, but the information has not been collected, it can greatly increase the difficulty of diagnosis, especially in complex situations like distributed systems such as microservices.

When our service becomes slow or the throughput decreases, it is also a path to check the most frequent exceptions. As for diagnosing slow backend issues, I will discuss it in more detail in the later Java Performance Basics module.

Today, I summarized the Java exception handling mechanism starting from a common concept of exception handling. I analyzed some generally accepted best practices and the latest consensus on exception usage in the industry, and finally, looked at the performance overhead of exceptions. I hope this will be helpful to you.

Practice Exercise #

Have you grasped the topic we discussed today? You can ponder on a question. Different programming paradigms can also influence exception handling strategies in programming. For example, in the currently popular Reactive Stream programming, which is asynchronous and event-driven, exceptions should not be simply thrown out. Additionally, due to the fact that the code stack is no longer a vertical structure of synchronous calls, exception handling and logging should be more careful. What we often see is the stack of a specific executor, rather than the invocation relationship of business methods. For this situation, do you have any good solutions?

Please share your solution in the comments section. I will select thoughtful comments and award you with a learning encouragement reward. I welcome you to join me in the discussion.

Are your friends also preparing for interviews? You can “invite your friends to read” and share today’s topic with them. Perhaps you can help them.