diff --git a/README.md b/README.md index 8941459..b3dfc23 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ Once the `Managed` stack is composed, the underlying resources are built and use Exception behavior is as follows: - Exceptions during setup are thrown after already-built resources are torn down - Exceptions during usage are thrown after resources are torn down -- Exceptions during teardown are thrown, but only after teardown is called on every resource. If an exception was thrown during usage, the teardown exceptions are added as suppressed exceptions on the usage exception. +- Exceptions during teardown are thrown, but only after teardown is called on every resource. +- If an exception is thrown during usage, and an exception occurred during teardown, the usage exception is thrown with the teardown exception added as a suppressed exception. +- If an exception is thrown during setup, and an exception occurred during teardown, the setup exception is thrown with the teardown exception added as a suppressed exception. For more details, see the Scaladocs. diff --git a/managerial/src/main/scala/ca/dvgi/managerial/Managed.scala b/managerial/src/main/scala/ca/dvgi/managerial/Managed.scala index 5b8429c..ecd361d 100644 --- a/managerial/src/main/scala/ca/dvgi/managerial/Managed.scala +++ b/managerial/src/main/scala/ca/dvgi/managerial/Managed.scala @@ -69,15 +69,21 @@ trait Managed[+T] { selfT => */ def flatMap[U](f: T => Managed[U]): Managed[U] = new Managed[U] { def build() = new Resource[U] { - val t = selfT.build() + private val t = selfT.build() - val u = + private val u = try { f(t.get).build() } catch { - case e: Exception => - t.teardown() - throw e + case setupThrowable: Throwable => + try { + t.teardown() + } catch { + case teardownThrowable: Throwable => + setupThrowable.addSuppressed(teardownThrowable) + } + + throw setupThrowable } def get = u.get diff --git a/managerial/src/test/scala/ca/dvgi/managerial/ManagedTest.scala b/managerial/src/test/scala/ca/dvgi/managerial/ManagedTest.scala index 88d4513..c32ec06 100644 --- a/managerial/src/test/scala/ca/dvgi/managerial/ManagedTest.scala +++ b/managerial/src/test/scala/ca/dvgi/managerial/ManagedTest.scala @@ -281,4 +281,33 @@ class ManagedTest extends munit.FunSuite { assertEquals(r, i) assert(tr.tornDown) } + + test( + "An exception in the Managed setup stack, followed by an exception in the teardown stack, surfaces the setup exception with a suppressed teardown exception" + ) { + val setupException = new RuntimeException("setup exception") + val teardownException = new RuntimeException("teardown exception") + + val m = for { + _ <- Managed.evalTeardown(throw teardownException) + _ <- Managed.evalSetup(throw setupException) + } yield () + + try { + m.build() + } catch { + case t: RuntimeException => + assertEquals(t.getMessage, setupException.getMessage) + assertEquals(t.getSuppressed.size, 1) + t.getSuppressed()(0) match { + case te: RuntimeException => + assertEquals( + te.getMessage, + teardownException.getMessage + ) + case e => fail("Unexpected exception", e) + } + case _: Throwable => fail("Unexpected exception") + } + } }