9

When an exception occurs during the handling of an exception, only the last exception gets reported, because I can add just one exception to an Error object. How to report all exception in the final error message?

Example:

class main
{
  public static void main (String[] args)
  {
    try {
      // Database.insert ();
      throw new Exception ("insert failed");
    }
    catch (Exception ex1) {
      try {
        // Database.rollback ();
        throw new Exception ("rollback failed");
      }
      catch (Exception ex2) {
        throw new Error ("Can not roll back transaction.", ex2);
      }
    }
    finally {
      try {
        // Database.close ();
        throw new Exception ("close failed");
      }
      catch (Exception ex3) {
        throw new Error ("Can not close database.", ex3);
      }
    }
  }
}

In the example everything fails. The database insert causes ex1. The rollback causes ex2. And closing the database causes ex3. When the program gets executed only the last ex3 gets reported in the Error object. How to include also ex1 and ex2 to the Error object? The constructor of Error accepts just one exception.

ceving
  • 21,900
  • 13
  • 104
  • 178

4 Answers4

12

I suggest you to use try-with-resource-statements introduced in Java 7, in conjunction with the AutoCloseable-interface.

Sidenote: The Connection, Statement and ResultSet from java.sql all implement AutoCloseable

try (Connection c = DriverManager.getConnection(url)) {
    // do your inserts
} catch (Exception e) {
    throw new Error("Insert failed", e);
}

This will close your connection or generally the passed AutoCloseable appropriately. And will handle the shadowing of the exception by using the Throwable.addSuppressed() method.

What the equivalent looks like in previous versions can be seen on this other question


Your questions also mentiones a rollback which I haven't covered. This can be done by using the before mentioned Throwable.addSuppressed() method (as pointed out in the comments by tobias_k), though to be honest it gets quite a bit more messy, and doesn't look as nice anymore:

Exception insertException = null;
try (Connection c = DriverManager.getConnection(url)) {
    try {
        // do your inserts
    } catch (Exception e1) {
        insertException = e1;
        // do your rollback
    }
} catch (Exception e2) {
    Error error = new Error("Insert failed", insertException);
    error.addSuppressed(e2);
    throw error;
}

Just for clarification, the inner catch-block, can only be reached when the insert fails. Where as the outer catch can be reached, when any of the following throws an exception:

  • DriverManager.getConnection()
  • Your rollback
  • Connection.close()

For a small demo you can visit this link, which illustrates how the stacktrace looks like.

Lino
  • 19,604
  • 6
  • 47
  • 65
  • 1
    IMHO the more general answer would be to use `addSuppressed` to add the "shadowed" exception, and that in many cases AutoCloseable can be used which will do all this for you. – tobias_k Aug 27 '20 at 08:41
  • @tobias_k How to use `addSuppressed` in my code? When I handle the exception, I do not know, that it will be suppressed, by an exception, which occurs later. – ceving Aug 27 '20 at 08:57
  • How to know how the exception handling in the AutoCloseable code works? And how to be sure, that the earlier exceptions are really added to the list of suppressed exceptions? – ceving Aug 27 '20 at 09:10
  • 1
    @Lino I do not understand why "rollback" suppresses "close". For me two report alternatives seem feasible: either report the first suppressing the later or report the last suppressing the earlier. But reporting the one in the middle suppressing the first and the last looks a bit confusing. – ceving Aug 27 '20 at 10:19
  • @ceving you're right, I've changed the order of the suppression. It now looks like [this](https://ideone.com/8eZNVe) – Lino Aug 27 '20 at 10:32
2

What you refer to is called suppressed exceptions in Java.

Starting from Java SE 7 there is a try-with-resources statement which automatically handles exceptions thrown within it. In your example it can be used like this:

class main
{
  public static void main (String[] args)
  {
    try(Database db = new Database()){ //ex3 can be thrown during closing the resource
      try
      {
        // db.insert ();
        throw new Exception ("insert failed");
      }
      catch (Exception ex1) {
        try {
          // db.rollback ();
          throw new Exception ("rollback failed");
        }
        catch (Exception ex2) {
          throw new Error ("Can not roll back transaction.", ex2);
        }
      }
    }
  }
}

In this case, if ex1 and ex3 are thrown, you get ex1 with ex3 in the list of suppressed exceptions.

If ex1, ex2 and ex3 are thrown, you get ex1 chained with ex2, and with ex3 in the list of suppressed exceptions.

Lino
  • 19,604
  • 6
  • 47
  • 65
Evgeny Mamaev
  • 1,237
  • 1
  • 14
  • 31
1

I updated my code based on Lino's answer.

class main
{
  public static void main (String[] args)
  {
    Throwable suppressed = null;

    try {
      // Database.insert ();
      if ("fail".equals(args[0]))
        throw new Exception ("insert failed");
    }
    catch (Exception ex1) {
      suppressed = ex1;
      try {
        // Database.rollback ();
        if ("fail".equals(args[1]))
          throw new Exception ("rollback failed");
        throw new Error ("Can not insert into database.", ex1);
      }
      catch (Exception ex2) {
        ex2.addSuppressed (suppressed);
        suppressed = ex2;
        throw new Error ("Can not roll back transaction.", ex2);
      }
    }
    finally {
      try {
        // Database.close ();
        if ("fail".equals(args[2]))
          throw new Exception ("close failed");
      }
      catch (Exception ex3) {
        if (suppressed != null)
          ex3.addSuppressed (suppressed);
        throw new Error ("Can not close database.", ex3);
      }
    }
  }
}

Now all four different failures are reported correctly.

  1. Insert fails
$ java -cp . main fail ok ok
Exception in thread "main" java.lang.Error: Can not insert into database.
        at main.main(main.java:18)
Caused by: java.lang.Exception: insert failed
        at main.main(main.java:10)
  1. Insert and Rollback fail
$ java -cp . main fail fail ok
Exception in thread "main" java.lang.Error: Can not roll back transaction.
        at main.main(main.java:23)
Caused by: java.lang.Exception: rollback failed
        at main.main(main.java:17)
        Suppressed: java.lang.Exception: insert failed
                at main.main(main.java:10)
  1. Insert, Rollback and Close fail
$ java -cp . main fail fail fail
Exception in thread "main" java.lang.Error: Can not close database.
        at main.main(main.java:35)
Caused by: java.lang.Exception: close failed
        at main.main(main.java:30)
        Suppressed: java.lang.Exception: rollback failed
                at main.main(main.java:17)
                Suppressed: java.lang.Exception: insert failed
                        at main.main(main.java:10)
  1. Close fails
$ java -cp . main ok ok fail
Exception in thread "main" java.lang.Error: Can not close database.
        at main.main(main.java:35)
Caused by: java.lang.Exception: close failed
        at main.main(main.java:30)
ceving
  • 21,900
  • 13
  • 104
  • 178
0

Why do you possibly need such a structure? In general, I firstly would suggest you to use try-with-resources. It will simplify your code significantly. Secondly, to handle this case, you have to design exception hierarchy, so you won't be using Exception class everywhere, which is obvious anti-pattern, but will thrown a new type of exceptions, e.g. instead of throwing new Exception ("close failed"), defined CloseFailedException that will inherit exception, and thrown it (throw new CloseFailedException ("close failed")).

  • I wrote a [mcve] and in order to keep it minimal, I avoided to write a bunch of exception classes. I also did not insert anything in a database. – ceving Aug 27 '20 at 09:00