no-exception
Introduction to NoException
1. Overview
In this article, we’ll focus on NoException which provides concise and handy exception handlers.
2. Maven Dependency
Let’s add the NoException to our pom.xml:
<dependency>
<groupId>com.machinezoo.noexception</groupId>
<artifactId>noexception</artifactId>
<version>1.1.0</version>
</dependency>
3. Standard Exception Handling
private static Logger logger = LoggerFactory.getLogger(NoExceptionUnitTest.class);
@Test
public void whenStdExceptionHandling_thenCatchAndLog() {
try {
logger.info("Result is " + Integer.parseInt("foobar"));
} catch (Throwable exception) {
logger.error("Caught exception:", exception);
}
}
We start by allocating a Logger and then entering a try block. If an Exception is thrown, we log it:
09:29:28.140 [main] ERROR c.b.n.NoExceptionUnitTest
- Caught exception
j.l.NumberFormatException: For input string: "foobar"
at j.l.NumberFormatException.forInputString(NumberFormatException.java:65)
at j.l.Integer.parseInt(Integer.java:580)
...
4. Handling Exceptions with NoException
@Test
public void whenDefaultNoException_thenCatchAndLog() {
Exceptions
.log()
.run(() -> System.out.println("Result is " + Integer.parseInt("foobar")));
}
This code gives us almost the same output as above:
09:36:04.461 [main] ERROR c.m.n.Exceptions
- Caught exception
j.l.NumberFormatException: For input string: "foobar"
at j.l.NumberFormatException.forInputString(NumberFormatException.java:65)
at j.l.Integer.parseInt(Integer.java:580)
...
In its most basic form, NoException provides us with a way to replace try/catch/ exceptions with a single line of code. It executes the lambda that we pass to run(), and if an Exception gets thrown, it gets logged.
4.2. Adding a Custom Logger
If we look closely at the output, we see that exceptions get logged as the logging class, instead of ours.
We can fix that, by providing our logger:
@Test
public void whenDefaultNoException_thenCatchAndLogWithClassName() {
Exceptions
.log(logger)
.run(() -> System.out.println("Result is " + Integer.parseInt("foobar")));
}
Which gives us this output:
09:55:23.724 [main] ERROR c.b.n.NoExceptionUnitTest
- Caught exception
j.l.NumberFormatException: For input string: "foobar"
at j.l.NumberFormatException.forInputString(NumberFormatException.java:65)
at j.l.Integer.parseInt(Integer.java:580)
...
4.3. Supplying a Custom Log Message
We may want to use a different message than the default “Caught Exception.” We can do this by passing a Logger as the first argument and a String message as the second:
@Test
public void whenDefaultNoException_thenCatchAndLogWithMessage() {
Exceptions
.log(logger, "Something went wrong:")
.run(() -> System.out.println("Result is " + Integer.parseInt("foobar")));
}
Which gives us this output:
09:55:23.724 [main] ERROR c.b.n.NoExceptionUnitTest
- Something went wrong:
j.l.NumberFormatException: For input string: "foobar"
at j.l.NumberFormatException.forInputString(NumberFormatException.java:65)
at j.l.Integer.parseInt(Integer.java:580)
...
But what if we want to do more than just log Exceptions, such as insert a fallback value when parseInt() fails?
4.4. Specifying a Default Value
Exceptions can return a result wrapped in an Optional. Let’s move things around so we can use it to provide a default value if the target fails:
@Test
public void
givenDefaultValue_whenDefaultNoException_thenCatchAndLogPrintDefault() {
System.out.println("Result is " + Exceptions
.log(logger, "Something went wrong:")
.get(() -> Integer.parseInt("foobar"))
.orElse(-1));
}
We still see our Exception:
12:02:26.388 [main] ERROR c.b.n.NoExceptionUnitTest
- Caught exception java.lang.NumberFormatException: For input string: "foobar"
at j.l.NumberFormatException.forInputString(NumberFormatException.java:65)
at j.l.Integer.parseInt(Integer.java:580)
...
But we also see our message printed to the console too:
Result is -1
5. Creating a Custom Logging Handler
So far we have a nice method of avoiding repetition and making code more readable in simple try/catch/log scenarios. What if we want to reuse a handler with a different behavior?
Let’s extend NoException‘s ExceptionHandler class and perform one of two things depending on the exception type:
public class CustomExceptionHandler extends ExceptionHandler {
Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);
@Override
public boolean handle(Throwable throwable) {
if (throwable.getClass().isAssignableFrom(RuntimeException.class)
|| throwable.getClass().isAssignableFrom(Error.class)) {
return false;
} else {
logger.error("Caught Exception", throwable);
return true;
}
}
}
By returning false when we see an Error or a RuntimeException we’re telling ExceptionHandler to re-throw. By returning true for everything else, we indicate that exception has been handled.
First, we’ll run this with a standard exception:
@Test
public void givenCustomHandler_whenError_thenRethrowError() {
CustomExceptionHandler customExceptionHandler = new CustomExceptionHandler();
customExceptionHandler.run(() -> "foo".charAt(5));
}
We pass our function to the run() method in our custom handler inherited from ExceptionHandler:
18:35:26.374 [main] ERROR c.b.n.CustomExceptionHandler
- Caught Exception
j.l.StringIndexOutOfBoundsException: String index out of range: 5
at j.l.String.charAt(String.java:658)
at c.b.n.CustomExceptionHandling.throwSomething(CustomExceptionHandling.java:20)
at c.b.n.CustomExceptionHandling.lambda$main$0(CustomExceptionHandling.java:10)
at c.m.n.ExceptionHandler.run(ExceptionHandler.java:1474)
at c.b.n.CustomExceptionHandling.main(CustomExceptionHandling.java:10)
This exception is logged. Let’s try with an Error:
@Test(expected = Error.class)
public void givenCustomHandler_whenException_thenCatchAndLog() {
CustomExceptionHandler customExceptionHandler = new CustomExceptionHandler();
customExceptionHandler.run(() -> throwError());
}
private static void throwError() {
throw new Error("This is very bad.");
}
And we see that the Error was re-thrown into main(), rather than logged:
Exception in thread "main" java.lang.Error: This is very bad.
at c.b.n.CustomExceptionHandling.throwSomething(CustomExceptionHandling.java:15)
at c.b.n.CustomExceptionHandling.lambda$main$0(CustomExceptionHandling.java:8)
at c.m.n.ExceptionHandler.run(ExceptionHandler.java:1474)
t c.b.n.CustomExceptionHandling.main(CustomExceptionHandling.java:8)
So we have a reusable class that can be used across an entire project for consistent exception handling.
6. Conclusion
With NoException we can simplify the exception handling on a case-by-case basis, with a single line of code.
The code can be found in this GitHub project.