“'defer' statement before end of scope always executes immediately.” Why?

I ended up writing some code that had a lot of possible exit conditions. There's one line of code that always needs to run when it exits, so I put it in a defer block near the top of the method.

There’s a line of code that needs to run at the end of the method (after everything else has succeeded), but must execute after the deferred statement earlier. So I put it inside a defer block at the very end of the method, and got a compiler warning:

'defer' statement before end of scope always executes immediately; replace with 'do' statement to silence this warning

I had expected that all deferred blocks (seen so far) would execute in sequence when scope exited. Why is a defer block at the end of scope treated differently? This seems like an inconsistency from the more desirable behavior I expected.

The deferred blocks are added to a stack and executed as they are popped off. If you need something to be executed last, you should defer it first.

3 Likes

Wow, really? That seems like the most counterintuitive, least useful way to do it.

It's actually useful when you want to pair allocation-deallocation code

func foo() {
  let data1 = ...
  defer { deallocate(data1) }

  let data2 = ...
  defer { deallocate(data2) }

  // Now deallocate 2
  // Now deallocate 1
}

Which can be especially tricky if data2 depends on data1.

Plus I usually put it as a reminds me to do this kinda code.
So I put it at the earliest line I realize I need to do something (usually cleanup), which I think is in the spirit of defer.

5 Likes

It makes a lot of sense when the defer statements build on one another:

func queryWithSocket(options: Options) -> Data {
    let socket = try Socket.requestFromSystem(options: Options)
    defer { socket.releaseToSystem() } // Needs to run last.

    try socket.setUp()
    defer { socket.tearDown() } // Needs to run 2nd last.

    try socket.open()
    defer { socket.close() ) // Needs to run 3rd last.

    return try socket.waitForResponse()
}

The order is integral to defer and part of what makes it so useful. It is designed particularly for paired constructors and destructors, where deconstruction order is—logically—opposite to construction order. It does not matter which try statement above fails, defer allows the abort process to safely back out in the right order and leave no mess behind.

6 Likes

I guess I can see that. But not so useful when you want to do this:

session.beginConfiguration()
defer { session.commitConfiguration() }    // must always be done

// some session configuration
if error { return }

// more session configuration
if error { return }

…

session.start()         //  Requires configuration be committed, so a  defer { session.start() } would be nice, if it went in order

I'd probably do it like this

let session = ...

do { // Configuration
  session.beginConfiguration()
  defer { session.commitConfiguration() }

  // some session configuration
  if error { return }

  // more session configuration
  if error { return }
}

…

session.start()
5 Likes

Ah, that's reasonable, but does force a whole level of indent for most of the method. In any case, not a bad solution.

I think it works from semantic standpoint.

You want to configure something, and commit regardless of what happens. It'd make sense that it forms a boundary that say Ok, configuration is done/has failed, what now?. It does add an indentation as you said. I personally think it'd be better to form its own function.

1 Like

I'd like to echo what you said about making configuration a separate function. In this example, configuration is logically separate from starting. In fact, I'd probably write the calling code something like this...

let session = try configureSession()
session.start()
2 Likes

Or, if you control the source of your "session" class, something like:

class MySessionClass {
    ...
    class func with(_ configBlock: (MySessionClass) throws -> ()) rethrows -> Self {
        let result = Self()
        result.beginConfiguration()
        defer { result.commitConfiguration() }

        try configBlock(result)
        return result
    }
    ...
}

Used like so:

let session = MySessionClass.with { (session) in
  // some session configuration
  if error { return }

  // more session configuration
  if error { return }
}