1. Overview
Sometimes try/catch blocks can result in verbose or even awkward code constructs.
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
Let’s start with a commonly-seen idiom:
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
4.1. Default Logging Handler
Let’s replace this with NoException‘s standard exception handler:
@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.