What is @escaping in Swift?

Published on: March 11, 2020

If you've ever written or used a function that accepts a closure as one of its arguments, it's likely that you've encountered the @escaping keyword. When a closure is marked as escaping in Swift, it means that the closure will outlive, or leave the scope that you've passed it to. Let's look at an example of a non-escaping closure:

func doSomething(using closure: () -> Void) {
  closure()
}

The closure passed to doSomething(using:) is executed immediately within the doSomething(using:) function. Because the closure is executed immediately within the scope of doSomething(using:) we know that nothing that we do inside of the closure can leak or outlive the scope of doSomething(using:). If we'd make the closure in doSomething(using:) outlive or leave the function scope, we'd get a compiler error:

func doSomething(using closure: () -> Void) {
  DispatchQueue.main.async {
    closure()
  }
}

The code above will cause a compiler error because Swift now sees that when we call doSomething(using:) the closure that's passed to this function will escape its scope. This means that we need to mark this as intentional so callers of doSomething(using:) will know that they're dealing with a closure that will outlive the scope of the function it's passed to which means that they need to take precautions like capturing self weakly. In addition to informing callers of doSomething(using:) about the escaping closure, it also tells the Swift compiler that we know the closure leaves the scope it was passed to, and that we're okay with that.

You will commonly see escaping closures for functions that perform asynchronous work and invoke the closure as a callback. For example, URLSession.dataTask(with:completionHandler:) has its completionHandler marked as @escaping because the closure passed as completion handler is executed once the request completes, which is some time after the data task is created. If you write code that takes a completion handler and uses a data task, the closure you accept has to be marked @escaping too:

func makeRequest(_ completion: @escaping (Result<(Data, URLResponse), Error>) -> Void) {
  URLSession.shared.dataTask(with: URL(string: "https://donnywals.com")!) { data, response, error in
    if let error = error {
      completion(.failure(error))
    } else if let data = data, let response = response {
      completion(.success((data, response)))
    } else {
      assertionFailure("We should either have an error or data + response.")
    }
  }
}

Tip:
I'm using Swift's Result type in this snippet. Read more about it in my post on using Result in Swift 5.

Notice that in the code above the completion closure is marked as @escaping. It has to be because I use it in the data task's completion handler. This means that the completion closure won't be executed until after makeRequest(_:) has exited its scope and the closure outlives it.

In short, @escaping is used to inform callers of a function that takes a closure that the closure might be stored or otherwise outlive the scope of the receiving function. This means that the caller must take precautions against retain cycles and memory leaks. It also tells the Swift compiler that this is intentional. If you have any questions about this tip or any other content on my blog, don't hesitate to send me a Tweet.

Categories

Quick Tip Swift

Subscribe to my newsletter