1. Types of exceptions

Types of exceptions

All exceptions are divided into 4 types, which are actually classes that inherit one another.

Throwable class

The base class for all exceptions is the Throwable class. The Throwable class contains the code that writes the current call stack (stack trace of the current method) to an array. We'll learn what a stack trace is a little later.

The throw operator can only accept an object that derives from the Throwable class. And although you can theoretically write code like throw new Throwable();, nobody usually does this. The main purpose of the Throwable class is to have a single parent class for all exceptions.

Error class

The next exception class is the Error class, which directly inherits the Throwable class. The Java machine creates objects of the Error class (and its descendants) when serious problems have occurred. For example, a hardware malfunction, insufficient memory, etc.

Usually, as a programmer, there is nothing you can do in a situation where such an error (the kind for which an Error should be thrown) has occurred in the program: these errors are too serious. All you can do is notify the user that the program is crashing and/or write all known information about the error to the program log.

Exception class

The Exception and RuntimeException classes are for common errors that happen in the operation of lots of methods. The goal of each thrown exception is to be caught by a catch block that knows how to properly handle it.

When a method cannot complete its work for some reason, it should immediately notify the calling method by throwing an exception of the appropriate type.

In other words, if a variable is equal to null, the method will throw a NullPointerException. If the incorrect arguments were passed to the method, it will throw an InvalidArgumentException. If the method accidentally divides by zero, it will throw an ArithmeticException.

RuntimeException class

RuntimeExceptions are a subset of Exceptions. We could even say that RuntimeException is a lightweight version of ordinary exceptions (Exception) — fewer requirements and restrictions are imposed on such exceptions

You'll learn the difference between Exception and RuntimeException later.


2. Throws: checked exceptions

Throws: checked exceptions

All Java exceptions fall into 2 categories: checked and unchecked.

All exceptions that inherit the RuntimeException or Error are considered unchecked exceptions. All others are checked exceptions.

Important!

Twenty years after checked exceptions were introduced, almost every Java programmer thinks of this as a bug. In popular modern frameworks, 95% of all exceptions are unchecked. The C# language, which almost copied Java exactly, did not add checked exceptions.

What is the main difference between checked and unchecked exceptions?

There are additional requirements imposed on checked exceptions. Roughly speaking, they are these:

Requirement 1

If a method throws a checked exception, it must indicate the type of exception in its signature. That way, every method that calls it is aware that this "meaningful exception" might occur in it.

Indicate checked exceptions after the method parameters after the throws keyword (don't use the throw keyword by mistake). It looks something like this:

type method (parameters) throws exception

Example:

checked exception unchecked exception
public void calculate(int n) throws Exception
{
   if (n == 0)
      throw new Exception("n is null!");
}
public void calculate(n)
{
   if (n == 0)
      throw new RuntimeException("n is null!");
}

In the example on the right, our code throws an unchecked exception — no additional action is required. In the example on the left, the method throws a checked exception, so the throws keyword is added to the method signature along with the type of the exception.

If a method expects to throw multiple checked exceptions, all of them must be specified after the throws keyword, separated by commas. The order is not important. Example:

public void calculate(int n) throws Exception, IOException
{
   if (n == 0)
      throw new Exception("n is null!");
   if (n == 1)
      throw new IOException("n is 1");
}

Requirement 2

If you call a method that has checked exceptions in its signature, you cannot ignore the fact that it throws them.

You must either catch all such exceptions by adding catch blocks for each one, or by adding them to a throws clause for your method.

It's as if we're saying, "These exceptions are so important that we must catch them. And if we do not know how to handle them, then anyone who might call our method must be notified that such exceptions can occur in it.

Example:

Imagine that we are writing a method to create a world populated by humans. The initial number of people is passed as an argument. So we need to add exceptions if there are too few people.

Creating Earth Note
public void createWorld(int n) throws EmptyWorldException, LonelyWorldException
{
   if (n == 0)
      throw new EmptyWorldException("There are no people!");
   if (n == 1)
      throw new LonelyWorldException ("There aren't enough people!");
   System.out.println("A wonderful world was created. Population: " + n);
}
The method potentially throws two checked exceptions:

  • EmptyWorldException
  • LonelyWorldException

This method call can be handled in 3 ways:

1. Don't catch any exceptions

This is most often done when the method does not know how to properly handle the situation.

Code Note
public void createPopulatedWorld(int population)
throws EmptyWorldException, LonelyWorldException
{
   createWorld(population);
}
The calling method does not catch the exceptions and must inform others about them: it adds them to its own throws clause

2. Catch some of the exceptions

We handle the errors we can handle. But the ones we don't understand, we throw them up to the calling method. To do this, we need to add their name to the throws clause:

Code Note
public void createNonEmptyWorld(int population)
throws EmptyWorldException
{
   try
   {
      createWorld(population);
   }
   catch (LonelyWorldException e)
   {
      e.printStackTrace();
   }
}
The caller catches only one checked exception — LonelyWorldException. The other exception must be added to its signature, indicating it after the throws keyword

3. Catch all exceptions

If the method does not throw exceptions to the calling method, then the calling method is always confident that everything worked well. And it will be unable to take any action to fix an exceptional situations.

Code Note
public void createAnyWorld(int population)
{
   try
   {
      createWorld(population);
   }
   catch (LonelyWorldException e)
   {
      e.printStackTrace();
   }
   catch (EmptyWorldException e)
   {
      e.printStackTrace();
   }
}
All exceptions are caught in this method. The caller will be confident that everything went well.


3. Wrapping exceptions

Checked exceptions seemed cool in theory, but turned out to be a huge frustration in practice.

Suppose you have a super popular method in your project. It is called from hundreds of places in your program. And you decide to add a new checked exception to it. And it may well be that this checked exception is really important and so special that only the main() method knows what to do if it is caught.

That means you'll have to add the checked exception to the throws clause of every method that calls your super popular method. As well as in the throws clause of all the methods that call those methods. And of the methods that call those methods.

As a result, the throws clauses of half of the methods in the project get a new checked exception. And of course your project is covered by tests, and now the tests don't compile. And now you have to edit the throws clauses in your tests as well.

And then all your code (all the changes in hundreds of files) will have to be reviewed by other programmers. And at this point we ask ourselves why we made we made so many bloody changes to the project? Day(s?) of work, and broken tests — all for the sake of adding one checked exception?

And of course, there are still problems related to inheritance and method overriding. The problems that come from checked exceptions are much larger than the benefit. The bottom line is that now few people love them and few people use them.

However there is still a lot of code (including standard Java library code) that contains these checked exceptions. What is to be done with them? We can't ignore them, and we don't know how to handle them.

Java programmers proposed to wrap checked exceptions in RuntimeException. In other words, catch all the checked exceptions and then create unchecked exceptions (for example, RuntimeException) and throw them instead. Doing that looks something like this:

try
{
   // Code where a checked exception might occur
}
catch(Exception exp)
{
   throw new RuntimeException(exp);
}

It's not a very pretty solution, but there's nothing criminal here: the exception was simply stuffed inside a RuntimeException.

If desired, you can easily retrieve it from there. Example:

Code Note
try
{
   // Code where we wrap the checked exception
   // in a RuntimeException
}
catch(RuntimeException e)
{
   Throwable cause = e.getCause();
   if (cause instanceof Exception)
   {
      Exception exp = (Exception) cause;
      // Exception handling code goes here
   }
}







Get the exception stored inside the RuntimeException object. The cause variable maybe null

Determine its type and convert it to a checked exception type.


4. Catching multiple exceptions

Programmers really hate to duplicate code. They even came up with a corresponding development principle: Don't Repeat Yourself (DRY). But when handling exceptions, there are frequent occasions when a try block is followed by several catch blocks with the same code.

Or there could be 3 catch blocks with the same code and another 2 catch blocks with other identical code. This is a standard situation when your project handles exceptions responsibly.

Starting with version 7, in the Java language added the ability to specify multiple types of exceptions in a single catch block. It looks something like this:

try
{
   // Code where an exception might occur
}
catch (ExceptionType1 | ExceptionType2 | ExceptionType3 name)
{
   // Exception handling code
}

You can have as many catch blocks as you want. However, a single catch block cannot specify exceptions that inherit one another. In other words, you cannot write catch (Exception | RuntimeException e), because the RuntimeException class inherits Exception.



5. Custom exceptions

You can always create your own exception class. You simply create a class that inherits the RuntimeException class. It will look something like this:

class ClassName extends RuntimeException
{
}

We'll discuss the details as you learn OOP, inheritance, constructors, and method overriding.

However, even if you only have a simple class like this (entirely without code), you can still throw exceptions based on it:

Code Note
class Solution
{
   public static void main(String[] args)
   {
      throw new MyException();
   }
}

class MyException extends RuntimeException
{
}




Throw an unchecked MyException.

In the Java Multithreading quest, we will take a deep dive into working with our own custom exceptions.