Swift
callback functions
Swift syntax
Swift programming
iOS development

Callback function syntax in Swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Swift, what most people call a callback is usually a closure passed into a function. Understanding the syntax matters because the same feature shows up in completion handlers, asynchronous APIs, error handling, animation blocks, and many other parts of Swift code.

The Basic Closure Type

A callback that takes no arguments and returns nothing has this type:

swift
() -> Void

A function can accept it like this:

swift
1func runLater(_ callback: () -> Void) {
2    print("before callback")
3    callback()
4    print("after callback")
5}
6
7runLater {
8    print("inside callback")
9}

This is the simplest form of callback syntax in Swift.

Callbacks with Parameters

Most real callbacks pass information back to the caller.

swift
1func fetchName(completion: (String) -> Void) {
2    let name = "Ada"
3    completion(name)
4}
5
6fetchName { name in
7    print("received:", name)
8}

The closure type here is:

swift
(String) -> Void

That means the callback receives a String and returns nothing.

Callbacks with Success and Failure Information

A common older Swift pattern is to pass optional result and optional error values into a completion handler.

swift
1enum DataError: Error {
2    case notFound
3}
4
5func loadValue(completion: (String?, Error?) -> Void) {
6    let ok = true
7    if ok {
8        completion("loaded", nil)
9    } else {
10        completion(nil, DataError.notFound)
11    }
12}
13
14loadValue { value, error in
15    if let value = value {
16        print(value)
17    } else {
18        print(error!)
19    }
20}

This works, but modern Swift usually prefers Result because it is clearer.

Prefer Result for Typed Outcomes

swift
1enum LoadError: Error {
2    case badResponse
3}
4
5func loadUser(completion: (Result<String, LoadError>) -> Void) {
6    completion(.success("Grace"))
7}
8
9loadUser { result in
10    switch result {
11    case .success(let user):
12        print("user:", user)
13    case .failure(let error):
14        print("error:", error)
15    }
16}

This makes the contract explicit: the callback returns either a success value or a failure value, but not both at once.

Mark Escaping Closures Correctly

If the callback is stored or called after the function returns, Swift requires @escaping.

swift
1import Foundation
2
3func runAsync(completion: @escaping () -> Void) {
4    DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
5        completion()
6    }
7}
8
9runAsync {
10    print("called later")
11}

Without @escaping, the compiler assumes the closure is used only during the function call itself.

Trailing Closure Syntax

Swift supports trailing closures when the final argument is a closure. That is why callbacks often look like this:

swift
fetchName { name in
    print(name)
}

instead of this:

swift
fetchName(completion: { name in
    print(name)
})

Both are valid. The first is just more idiomatic when the closure is the last argument.

Async and Await Reduce Callback Noise

Modern Swift increasingly replaces callback-heavy APIs with async and await.

swift
1func fetchNameAsync() async -> String {
2    "Ada"
3}
4
5Task {
6    let name = await fetchNameAsync()
7    print(name)
8}

Even so, callback syntax still matters because many Apple and third-party APIs still use closures, and async code is often built on top of them.

Avoid Retain Cycles in Stored Callbacks

If a closure captures self and is retained by self, you can create a retain cycle. In those cases use a capture list.

swift
1class Loader {
2    var onFinish: (() -> Void)?
3
4    func configure() {
5        onFinish = { [weak self] in
6            print(self != nil)
7        }
8    }
9}

This is not specific to callbacks, but callback-based designs hit this issue often enough that it belongs in the syntax discussion.

Common Pitfalls

The most common mistake is forgetting @escaping when the callback runs later. The compiler will usually catch this, but the reason matters.

Another issue is using the older optional-value plus optional-error pattern when Result would express the contract more clearly.

A third problem is misunderstanding trailing closure syntax and thinking it changes the meaning of the callback. It does not. It only changes how the call is written.

Summary

  • In Swift, callbacks are usually closures passed as function parameters.
  • A simple callback type looks like () -> Void or (Value) -> Void.
  • Use @escaping when the callback outlives the function call.
  • Prefer Result over parallel optional success and error parameters.
  • Learn trailing closure syntax because it is the common style in Swift APIs.

Course illustration
Course illustration

All Rights Reserved.