Initial situation
Our project contains complex business logic with several asynchronous steps. In this specific case, this means that some errors are passed as suppressed exceptions. As several exceptions can occur, we would like to transform all of them into a helpful response for the interface user.

Our goal here is to determine the root causes of the exceptions. There is a helper method from Apache Commons for this, but in our case we also have to consider all suppressed exceptions in the hierarchy, which requires a custom implementation.
Safeguarding by generating further tests
Our first own implementation seems to work completely, all existing tests are green. To be on the safe side, we use ChatGPT to write unit tests for the given utility method.
This works almost completely, only minor corrections to the naming are necessary.
We extend one of the test methods manually, as the ChatGPT variant only considered one suppressed exception in a complex scenario with an exception hierarchy that also contains suppressed exceptions.
ChatGPT used standard assertions from JUnit, while we prefer AssertJ assertions. Through the prompt "Use AssertJ assertions" the code is adapted correctly.
In addition, local variables should be final must be declared. A prompt also helps here: "Use final wherever possible".
This ultimately leads to the following test class:
class RootCauseFinderTest {
@Test
public void testSingleThrowable() {
final Throwable t = new Throwable("Root cause message");
final List messages = RootCauseFinder.getRootErrorMessages(t);
assertThat(messages).containsExactly("Root cause message");
}
@Test
public void testMultipleThrowables() {
final Throwable t1 = new Throwable("Root cause message 1");
final Throwable t2 = new Throwable("Root cause message 2");
final List messages = RootCauseFinder.getRootErrorMessages(t1, t2);
assertThat(messages).containsExactlyInAnyOrder("Root cause message 1", "Root cause message 2");
}
@Test
public void testNestedThrowable() {
final Throwable rootCause = new Throwable("Root cause message");
final Throwable exception = new Throwable("My exception", rootCause);
final List messages = RootCauseFinder.getRootErrorMessages(exception);
assertThat(messages).containsExactly("Root cause message");
}
@Test
public void testSuppressedThrowable() {
final Throwable suppressed = new Throwable("Suppressed message");
final Throwable main = new Throwable("Main exception");
main.addSuppressed(suppressed);
final List messages = RootCauseFinder.getRootErrorMessages(main);
assertThat(messages).containsExactlyInAnyOrder("Main exception", "Suppressed message");
}
@Test
public void testDistinctMessages() {
final Throwable t1 = new Throwable("Same message");
final Throwable t2 = new Throwable("Same message");
final List messages = RootCauseFinder.getRootErrorMessages(t1, t2);
assertThat(messages).containsExactly("Same message");
}
@Test
public void testComplexExceptionHierarchy() {
final Throwable rootCause = new Throwable("Root cause message");
final Throwable rootCauseFromSuppressed1 = new Throwable("Root cause message from suppressed 1");
final Throwable rootCauseFromSuppressed2 = new Throwable("Root cause message from suppressed 2");
final Throwable suppressed1 = new Throwable("Suppressed message 1", rootCauseFromSuppressed1);
final Throwable suppressed2 = new Throwable("Suppressed message 2", rootCauseFromSuppressed2);
final Throwable exception = new Throwable("My exception", rootCause);
exception.addSuppressed(suppressed1);
exception.addSuppressed(suppressed2);
final List messages = RootCauseFinder.getRootErrorMessages(exception);
assertThat(messages).containsExactlyInAnyOrder("Root cause message", "Root cause message from suppressed 1", "Root cause message from suppressed 2");
}
@Test
public void testComplexExceptionHierarchy_duplicateRootCauseFromSuppressedExceptions() {
final Throwable rootCause = new Throwable("Root cause message");
final Throwable rootCauseFromSuppressed1 = new Throwable("Root cause message from suppressed");
final Throwable rootCauseFromSuppressed2 = new Throwable("Root cause message from suppressed");
final Throwable suppressed1 = new Throwable("Suppressed message", rootCauseFromSuppressed1);
final Throwable suppressed2 = new Throwable("Suppressed message 2", rootCauseFromSuppressed2);
final Throwable exception = new Throwable("My exception", rootCause);
exception.addSuppressed(suppressed1);
exception.addSuppressed(suppressed2);
final List messages = RootCauseFinder.getRootErrorMessages(exception);
assertThat(messages).containsExactlyInAnyOrder("Root cause message", "Root cause message from suppressed");
}
@Test
public void testEmptyInput() {
final List messages = RootCauseFinder.getRootErrorMessages();
assertThat(messages).isEmpty();
}
}
Error discovered
When running the tests, we notice that our business logic does not work completely.
Let's let ChatGPT code
Instead of correcting our own implementation ourselves, we try,
to generate the method again using ChatGPT.
Prompt: "Java: From a given throwable I need to go down the hierarchy and find the root cause. If there are suppressed exceptions their root causes must also be collected. Result is a list of Strings containing the root cause messages."
import java.util.ArrayList;
import java.util.List;
public class RootCauseFinder {
public static List getRootCauseMessages(Throwable throwable) {
List rootCauseMessages = new ArrayList<>();
collectRootCauseMessages(throwable, rootCauseMessages);
return rootCauseMessages;
}
private static void collectRootCauseMessages(Throwable throwable, List rootCauseMessages) {
if (throwable == null) {
return;
}
// Navigate to the root cause of the current throwable
Throwable rootCause = throwable;
while (rootCause.getCause() != null) {
rootCause = rootCause.getCause();
}
// Add the message of the root cause to the list
rootCauseMessages.add(rootCause.getMessage());
// Check for suppressed exceptions and process their root causes
for (Throwable suppressed : throwable.getSuppressed()) {
collectRootCauseMessages(suppressed, rootCauseMessages);
}
// Check for the cause of the current throwable and process its root causes
if (throwable.getCause() != null) {
collectRootCauseMessages(throwable.getCause(), rootCauseMessages);
}
}
}
Optimization
The generated code is pure Java code. Since we are using Apache Commons, the code can be deployed using ExceptionUtils.getRootCause shorten.
Prompt: "Use Apache Commons ExceptionUtils.getRootCause where possible":
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.ArrayList;
import java.util.List;
public class RootCauseFinder {
public static List getRootCauseMessages(Throwable throwable) {
List rootCauseMessages = new ArrayList<>();
collectRootCauseMessages(throwable, rootCauseMessages);
return rootCauseMessages;
}
private static void collectRootCauseMessages(Throwable throwable, List rootCauseMessages) {
if (throwable == null) {
return;
}
// Use Apache Commons ExceptionUtils to get the root cause
Throwable rootCause = ExceptionUtils.getRootCause(throwable);
if (rootCause == null) {
rootCause = throwable;
}
// Add the message of the root cause to the list
rootCauseMessages.add(rootCause.getMessage());
// Check for suppressed exceptions and process their root causes
for (Throwable suppressed : throwable.getSuppressed()) {
collectRootCauseMessages(suppressed, rootCauseMessages);
}
// Check for the cause of the current throwable and process its root causes
if (throwable.getCause() != null) {
collectRootCauseMessages(throwable.getCause(), rootCauseMessages);
}
}
}
In our business logic, we need to determine the root causes of several throwables. We had left this out of the original prompt so that the description would not be too complex. It is also trivial to add this manually.
public static List getRootErrorMessages(final Throwable... throwables) {
return Arrays.stream(throwables)
.flatMap((Throwable throwable) -> getRootCauseMessages(throwable).stream())
.distinct()
.toList();
}
Simplification and improvement
Through the prompt "Can this code be simplified?",
where we pass the current code state of the class, the code is simplified.
However, parameters and local variables are not declared as final.
Another prompt "Add finals to local variables and parameters" leads to the following result:
public static List getRootErrorMessages(final Throwable... throwables) {
return Arrays.stream(throwables)
.flatMap((Throwable throwable) -> getRootCauseMessages(throwable).stream())
.distinct()
.toList();
}
public static List getRootCauseMessages(final Throwable throwable) {
final List rootCauseMessages = new ArrayList<>();
collectRootCauseMessages(throwable, rootCauseMessages);
return rootCauseMessages;
}
private static void collectRootCauseMessages(final Throwable throwable, final List rootCauseMessages) {
if (throwable == null) return;
final Throwable rootCause = ExceptionUtils.getRootCause(throwable);
rootCauseMessages.add((rootCause != null ? rootCause : throwable).getMessage());
for (final Throwable suppressed : throwable.getSuppressed()) {
collectRootCauseMessages(suppressed, rootCauseMessages);
}
collectRootCauseMessages(throwable.getCause(), rootCauseMessages);
}
Another prompt "Can the code be changed so collectRootCauseMessages collects the throwables instead. Get the message strings in getRootCauseMessages instead. Rename the method to reflect the change." executes the desired refactoring.
We also want some documentation, hence another prompt: "Add javadoc".
This results in the following final version:
/**
* Retrieves a list of root cause messages from a throwable.
*
* @param throwable the throwable to process
* @return a list of root cause messages
*/
public static List getRootCauseMessages(final Throwable throwable) {
final List rootCauses = new ArrayList<>();
collectRootCauses(throwable, rootCauses);
return rootCauses.stream()
.map(Throwable::getMessage)
.distinct()
.toList();
}
/**
* Recursively collects root causes from a throwable and adds them to the provided list.
*
* @param throwable the throwable to process
* @param rootCauses the list to add the root causes to
*/
private static void collectRootCauses(final Throwable throwable, final List rootCauses) {
if (throwable == null) return;
final Throwable rootCause = ExceptionUtils.getRootCause(throwable);
rootCauses.add(rootCause != null ? rootCause : throwable);
for (final Throwable suppressed : throwable.getSuppressed()) {
collectRootCauses(suppressed, rootCauses);
}
collectRootCauses(throwable.getCause(), rootCauses);
}
Tip
Check in the code after each optimization step. This makes it easier to check and understand the changes made using the diff function in the IDE.
Conclusion
AI is now an integral part of development. Thinking for yourself is not superfluous here, because the formulation of the prompts is crucial and the results have to be checked and sometimes adjusted manually. It is more like pair programming, where a better solution is developed together step by step and the strengths of the AI are used together with the developer's experience.
The AI does a lot of the hard work, such as writing Javadoc or adding missing tests. The latter is extremely helpful for refactorings in order to achieve meaningful and good test coverage.
It is useful for quality assurance to simplify the code with the help of AI
and to incorporate various improvements,
such as the refactoring and renaming shown above or the addition of finals.
Code and image generated with the help of ChatGPT.



