In Java, an exception is a mechanism to signal that the execution of a method can not be performed by example, trying to get a value of a list with an index equals to -1
String valueAt(List<String> list, int index) {
return list.get(index);
}
valueAt(List.of("hello"), -1);
an exception has
- a type that indicate the kind of error
- an error message that explain in English the issue
- a stacktrace which indicates where the exception was raised and the methods to reach that point
In our example, java.lang.IndexOutOfBoundsException
is the type,
Index: -1 Size: 1
is the message and
at ImmutableCollections$AbstractImmutableList.outOfBounds (ImmutableCollections.java:201)
at ImmutableCollections$List12.get (ImmutableCollections.java:418)
at valueAt (#1:2)
at (#2:1)
is the stacktrace
You can create (with new
) and raise your own exception using the keyword throw
String valueAt(List<String> list, int index) {
if (index < 0 || index >= list.size()) {
throw new IllegalArgumentException("invalid index " + index);
}
return list.get(index);
}
valueAt(List.of("hello"), -1);
The stacktrace is populated automatically when you create the exception
not where you throw it so it's a good idea to create the exception
not too far from where you throw it.
In the following example, the stacktrace will say that the exception
is created at notTooFar (#5:2)
, on the second line, not at notTooFar (#5:4)
.
void notTooFar() {
var exception = new RuntimeException("i'm created here");
// an empty line
throw exception;
}
notTooFar();
While you can create your own exception (see below), usually we are re-using already existing exceptions.
Exceptions commonly used in Java
- NullPointerException if a reference is null
- IllegalArgumentException if an argument of a method is not valid
- IllegalStateException if the object state doesn't allow to proceed, by example if a file is closed, you can not read it
- AssertionError if a code that should not be reached has been reached
By example
enum State { OK, NOT_OK }
void testState(State state) {
switch(state) {
case OK -> System.out.println("Cool !");
case NOT_OK -> System.out.println("Not cool");
default -> { throw new AssertionError("Danger, Will Robinson"); }
}
}
here the AssertionError can only be thrown if the code if testState() and the enum State disagree on set of possible values By example, if a new state is added
enum State { OK, NOT_OK, UNKNOWN }
testState(State.UNKNOWN);
In Java, you can recover from an exception using a try/catch
block.
URI uri;
try {
uri = new URI("http://i'm a malformed uri");
} catch(URISyntaxException e) {
// if the URI is malformed, used google by default
uri = new URI("http://www.google.fr");
}
System.out.println(uri);
A common mistake is to write a try/catch
in a method with an empty catch
or a catch that log/print a message instead of actually recovering from the
exception
As a rule of thumb, if you can not write something meaningful in the catch
block then you should not use a try/catch
.
For the compiler, there are two kinds of exceptions that are handled differently
- unchecked exception, you can throw them anywhere you want
- checked exception, you can only throw them if
- you are inside a method that declare to throws that exception (or a supertype)
- you are inside a try/catch block on that exception (or a supertype)
In Java, an exception that inherits from RuntimeException
or Error
are
unchecked exceptions, all the others are checked exceptions
so this code doesn't compile because IOException
inherits from Exception
and not RuntimeException
.
/*
void hello() {
Files.delete(Path.of("I don't exist"));
}
*/
A way to fix the issue is to use the keywords throws
to ask the caller
of the method to deal with the exception, again the caller will have,
either by propagating it with a throws
or recover from it with a try/catch
.
void hello() throws IOException {
Files.delete(Path.of("I don't exist"));
}
As a rule of thumb, 99% of the time you want to propagate the exception,
and keep the number of try/catch
as low as possible in your program,
so prefer throws
to try/catch
.
If a method has it's signature fixed because it overrides a method of an interface,
then you can not use throws
The following example doesn't compile because the method run
of a Runnable
doesn't declare to throws
IOException
so the only solution seems to be
to use a try/catch
.
/*
var aRunnable = new Runnable() {
public void run() {
Files.delete(Path.of("I don't exist"));
}
};
*/
So here, we have to use a try/catch
but we still want to propagate the exception.
The trick is wrap the checked exception into an unchecked exception.
This trick is so common that the Java API already comes with existing
classes to wrap common checked exceptions. For IOException
, the unchecked
equivalent is UncheckedIOException
.
var aRunnable = new Runnable() {
public void run() {
try {
Files.delete(Path.of("I don't exist"));
} catch(IOException e) {
// the way to recover, is to propagate it as an unchecked
throw new UncheckedIOException(e);
}
}
};
aRunnable.run();
The exception UndeclaredThrowableException
is used as the generic unchecked exception
to wrap any checked exception which do not have an unchecked equivalent.
You can create your own exception by creating a class that inherits from RuntimeException
You should provide at least two constructors, one with a message and one with a message
and a cause.
public class MyException extends RuntimeException {
public MyException(String message) {
super(message);
}
public MyException(String message, Throwable cause) {
super(message, cause);
}
}
throw new MyException("This is my exception");
But in general, don't ! Reuse existing commonly used exceptions.