What happens when a close throws an exception - are the other resources still closed?

← Error Handling and Exceptions · Ref: Q1902

Every close still runs. Wherever EK9 generates closing - the end of a try, the end of a statement that made an unnamed closeable, and the chain after a type's 'operator close' - each close runs in its own protected region, last acquired first:

- if closes throw, the FIRST Exception is rethrown after all of them have run, and each later one is attached to it as SUPPRESSED - read them with 'ex.suppressed()', a List of Exception (QA1920), and an uncaught failure lists them as 'suppressed:' lines;
- if the try body (or the acquisition) already threw, THAT exception is the one that propagates, and every close failure is suppressed onto it - a close never replaces the real failure;
- a Panic is never caught there: a Panic in a close stops the remaining closes, and a Panic in the body still closes everything and then propagates unchanged.

A constructor that fails PART WAY runs the same chain over what it had acquired: every closeable property that is present is closed, last first, and then the exception is rethrown - so a half-built object never leaks the resources it already holds.

A close failure happens AFTER the try's own body and catch, so to catch it put the try-with-resources inside another try:

  try
    try
      -> idle <- Machine()
      stdout.println(`using ${idle?}`)
  catch
    -> ex as Exception
    stdout.println(`caught: ${ex.reason()}`)
    for failure in ex.suppressed()
      stdout.println(`suppressed: ${failure.reason()}`)

In the program below, Machine has three parts and two of their closes throw: all three are closed, the first failure ('bad third', closed first) is the one caught and 'bad second' is in its suppressed() list. When the work itself throws, the work's exception is caught instead, with the Machine's close failure suppressed onto it. And an Assembly whose constructor fails after its frame was acquired closes the frame before the exception arrives.

Example

defines module qa.errorhandling.whenaclosefails

  defines class

    Part
      name as String?

      default private Part()

      Part()
        -> label as String
        name: label

      operator close as pure
        stdout <- Stdout()
        stdout.println(`closing ${name}`)
        if name.startsWith("bad")
          throw Exception(`close of ${name} failed`)

      default operator ?

    Machine
      first as Part: Part("first")
      second as Part: Part("bad second")
      third as Part: Part("bad third")

      default operator close

      default operator ?

    Assembly
      frame as Part: Part("frame")
      motor as Part?

      default private Assembly()

      Assembly()
        -> fitMotor as Boolean
        if not fitMotor
          throw Exception("no motor to fit")
        motor: Part("motor")

      default operator close

      default operator ?

  defines program

    WhenACloseFails()
      stdout <- Stdout()

      try
        try
          -> idle <- Machine()
          stdout.println(`using ${idle?}`)
      catch
        -> ex as Exception
        stdout.println(`caught: ${ex.reason()}`)
        for failure in ex.suppressed()
          stdout.println(`suppressed: ${failure.reason()}`)

      try
        try
          -> busy <- Machine()
          require busy?
          throw Exception("the work failed")
      catch
        -> ex as Exception
        stdout.println(`caught: ${ex.reason()}`)
        for failure in ex.suppressed()
          stdout.println(`suppressed: ${failure.reason()}`)

      try
        try
          -> assembly <- Assembly(false)
          stdout.println(`assembled ${assembly?}`)
      catch
        -> ex as Exception
        stdout.println(`caught: ${ex.reason()}`)
Other ways to ask this
  • If one operator close fails, does EK9 skip the rest?
  • Which exception do I catch when the try body and a close both fail?
  • What does a constructor that fails part way do with the resources it acquired?
  • Does a close exception replace the exception my code threw?
  • Where do the close failures go that were not rethrown?

Coming from another language?

Java: try-with-resources closes every resource and adds later close failures with addSuppressed, but a hand-written close() that closes several fields stops at the first one that throws, and a constructor that throws leaks what it opened. C#: a throwing Dispose in a using block replaces the original exception. C++: a destructor that throws during unwinding terminates the program. EK9: the same rule everywhere closing is generated - every close runs, the first failure wins, later ones are suppressed, and a failed constructor closes what it acquired.

Keywords: try with resources, suppressed, Panic, close, operator close, default operator close, exception, constructor, partial construction