The defer
statement is a useful tool for guaranteeing the cleanup of resources when a scope is exited. Sometimes, it may be desirable to set up a scoped cleanup that runs if the scope is interrupted by an error, but not if the scope is exited successfully. For instance, you may be trying to perform a multiple-step transition between two valid states, where any of the multiple steps may fail, and want to make sure you don't get stuck in an invalid intermediate step. Right now, the facilities for expressing this are wanting. You can use a flag:
// Valid states are:
// stateA = 0; stateB = 0; stateC = 0
// -or-
// stateA = 6; stateB = 7; stateC = 9
var stateA = 0
var stateB = 0
var stateC = 0
func transitionStates() throws {
var didFinish = false
stateA = try computeSix()
defer { if !didFinish { stateA = 0 } }
stateB = try computeSeven()
defer { if !didFinish { stateB = 0 } }
stateC = try computeNine()
defer { if !didFinish { stateC = 0 } }
try finishTransition()
didFinish = true
}
but this leaves a lot in the hands of programmer discipline: as the code evolves, the programmer must remember to check if !didFinish
in every cleanup, and must remember to set the flag on every path where the transition is ready to be committed. An arguably more disciplined way to manage this is with nested catch
blocks:
func transitionStates() throws {
stateA = try computeSix()
do {
stateB = try computeSeven()
do {
stateC = try computeNine()
do {
try finishTransition()
} catch {
stateC = 0
throw error
}
} catch {
stateB = 0
throw error
}
} catch {
stateA = 0
throw error
}
}
This pattern reduces the room for programmer error (though it doesn't eliminate it completely—the programmer still has to remember to re-throw
the error
after every catch block), but it's painful to look at, being a "pyramid of doom" with confusingly-nested reverse-ordered catch
blocks, the same sorts of readability issues that led us to adopt Go-style defer
instead of Java-style finally
blocks in the original error handling model.
It would be nice if there was a form of defer
that runs only when the scope is exited because of a thrown error. I went ahead and prototyped this idea, using the strawman syntax defer catch
(a syntax I only used because it was easy to implement, and am in no way committed to). This allows the above example to be written straightforwardly:
func transitionStates() throws {
stateA = try computeSix()
defer catch { stateA = 0 }
stateB = try computeSeven()
defer catch { stateB = 0 }
stateC = try computeNine()
defer catch { stateC = 0 }
try finishTransition()
}
These sorts of multi-step state transitions are still best avoided if at all possible, of course. For a simple value-type-based example like this, you could combine stateA
, stateB
, and stateC
above into a single struct, have transitionStates
operate on a local copy of the struct while making incremental changes, and then assign the final value back once the transition is complete. However, when working in constrained environments, or with existing APIs that require parts of a transaction to be done in multiple possibly-throwing calls, it may not always be possible to avoid exposing intermediate states. Having a defer-on-error construct would fill in a useful expressivity gap for these situations.