[Pitch] `defer` statement that runs only on error

I haven't often encountered code that requires this granularity of state resetting. While recognizing that this a toy example, I'm working to understand where this improves upon e.g.

func transitionStates() throws {
  do {
    stateA = try computeSix()
    stateB = try computeSeven()
    stateC = try computeNine()
    try finishTransition()
  } catch {
    (stateA, stateB, stateC) = (0, 0, 0)
    throw error
  }
}

My impressions are:

  1. If the real-world replacement for stateA = 0 (i.e. a state reset) is expensive, unconditionally resetting all states regardless of the failure point could be more costly;
  2. In the circumstance that (1) holds, the existing syntactic alternative is, as described:

However, I think the syntactic burden of try extends beyond the particular use case described here. Consider comparable code like this, which is deliberately un-nested:

func performSteps() -> Output {
  let resultA: ResultA
  do {
    resultA = try stepA()
  } catch {
    return fallbackA
  }

  let resultB: ResultB
  do {
    resultB = try stepB(resultA: resultA)
  } catch {
    return fallbackB
  }

  // ...
}

The existing mechanism to un-nest sequential throwing operations with custom per-failure-point catch handling introduces 1) an otherwise unnecessary type annotation and 2) a fair amount of syntax (do/try/catch plus two sets of braces) for what roughly amounts to a throwing guard statement.

This is recognizedly unergonomic ([1], [2], [3], [4], [5], ...), and I suspect a syntactic solution to this challenge would largely address the problem described here. Strawman:

func transitionStates() throws {
  stateA = try computeSix() catch { 
    stateA = 0 
    throw error
  }

  stateB = try computeSeven() catch {
    (stateA, stateB) = (0, 0)
    throw error
  }
  
  stateC = try computeNine() catch {
    (stateA, stateB, stateC) = (0, 0, 0)
    throw error
  }

  try finishTransition()
}

Although this requires some duplication of cleanup logic:

  1. the order of operations is both unambiguous and linear (in contrast to sequenced defer blocks, which must be traced up backwards to interpret);
  2. the syntactic burden is alleviated comparably to the proposed solution;
  3. the example captured by performSteps() above benefits equally from the new syntax.
3 Likes