> ## Content Index
> Fetch the complete content index at: https://williamboles.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Avoid Queue-Jumping
- URL: https://williamboles.com/avoid-queue-jumping/
- Published: 2017-04-17T18:50:00.000Z
- Updated: 2026-09-26T16:34:29.000Z
- Description: We are often told that no matter where you end up, you shouldn't forget where you came from. Usually, this saying is applied to people, but it can also apply to code. Here, we look at how to capture the originating thread when kicking off a background task so that we can return to it later.
- Author: William Boles
- Tags: Concurrency

As humans, we are great at spotting patterns, but see that pattern often enough, and it becomes familiar and eventually, effectively invisible - that clock ticking, that smudge mark on the wall or that weird smell, we stop noticing them all. It's the same with code - that odd syntax or awkward call-site eventually stops standing out. That's why our projects are often cluttered with these oddities. We treat them as harmless because they become familiar, but they're not, especially for new developers trying to understand the project. `DispatchQueue.main` code blocks are one such pattern that we stop seeing. But having `DispatchQueue.main` code blocks scattered throughout our projects makes our code harder to understand and test.

![Photo showing people queuing](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2019/11/people-queuing-min-1.jpg)

This post will explore how we can cut that clutter by being more thoughtful about where we place those `DispatchQueue.main` calls.

## Limiting Queue-Jumping 🦘

It's not unusual to see queue-jumping throughout a project. Take this class that undertakes asynchronous work on a background queue and returns (via a closure) the result of that work:

```
class Worker {
    // 1
    func performWork(completion: @escaping (Result<String, Error>) -> Void) {
        // 2
        DispatchQueue.global().async {
            // Perform work

            // 3
            completion(.success("Done"))
        }
    }
}
```

Here's what we did above:

1. `performWork(completion:)` takes a single `completion` closure parameter. It uses [Result](https://developer.apple.com/documentation/swift/result?ref=williamboles.com) enum type to communicate success or failure back to the caller.
2. The work is dispatched onto a global background queue.
3. `completion` is triggered directly from inside the background queue.

Now let's see `performWork(_:)` being used in an example view-model:

```
class ViewModel {
    private let worker = Worker()

    func process() {
        worker.performWork { result in
            // 1
            DispatchQueue.main.async {
                switch result {
                case .success(let value):
                    // Do something
                case .failure(let error):
                    // Do something else
                }
            }
        }
    }
}
```

1. Because `performWork(_:)` doesn't offer any control over the callback queue, the caller has to manually move the response handling onto the `main` queue - this is queue-jumping.

Every call-site that cares about which queue it's called back on will need to include this same boilerplate.

We can do better than that 😎.

## Capturing the Queue

Let's update `Worker` to allow the caller to specify which queue to be called back on:

```
class Worker {
    // 1
    func performWork(callbackQueue: DispatchQueue,
                     completion: @escaping (Result<String, Error>) -> Void) {
        DispatchQueue.global().async {
            // Perform work

            // 2
            callbackQueue.async {
                completion(.success("Done"))
            }
        }
    }
}
```

1. `callbackQueue` controls which queue the `completion` fires on - we're shifting control of execution context from the callee to the caller.
2. Rather than just directly triggering `completion`, that triggering is now scheduled asynchronously on `callbackQueue` \- ensuring the caller receives the callback on the queue they specified.

> If this approach feels familiar, you might be thinking of the `delegateQueue` parameter on the [URLSession](https://developer.apple.com/documentation/foundation/urlsession/init%28configuration:delegate:delegatequeue:%29?ref=williamboles.com) which I've shamelessly been inspired by.

With `Worker` absorbing the queue-jumping code, we can simplify each call-site:

```
class ViewModel {
    private let worker = Worker()

    func process() {
        worker.performWork(callbackQueue: .main) { result in
            switch result {
            case .success(let value):
                // Do something
            case .failure(let error):
                // Do something else
            }
        }
    }
}
```

If we wanted to instead have `result` processed on another queue, it would look like:

```
class ViewModel {
    private let worker = Worker()
    private let processingQueue = DispatchQueue(label: "com.williamboles.processing")

    func process() {
        worker.performWork(callbackQueue: processingQueue) { result in
            switch result {
            case .success(let value):
                // Do something
            case .failure(let error):
                // Do something else
            }
        }
    }
}
```

With `callbackQueue` handling the dispatch, we've pushed the queue-jumping out of the call-site, and the responsibilities of `process()` are easier to see.

The `callbackQueue` parameter has already simplified our call-sites, but every caller still has to think about which queue they want. In practice, the most common use-case will be `.main`. So rather than making every call-site state the queue, we can default `callbackQueue` to `.main`:

```
class Worker {
    func performWork(callbackQueue: DispatchQueue = .main,
                     completion: @escaping (Result<String, Error>) -> Void) {
        // Omitted unchanged functionality
    }
}
```

With this change our call-sites can now look like:

```
class ViewModel {
    private let worker = Worker()

    func process() {
        worker.performWork { result in
            switch result {
            case .success(let value):
                // Do something
            case .failure(let error):
                // Do something else
            }
        }
    }
}
```

Now, callers that need the main queue don't need to think about queues at all and those that don't want the main queue can easily override the default. We've reduced friction without removing flexibility.

When the queue is hardcoded inside the method, there's no reliable way for a test to verify which queue the callback fired on. By making the callback queue injectable, we bring that behaviour under test:

```
final class WorkerTests: XCTestCase {
    // 1
    var sut: Worker!

    override func setUp() {
        super.setUp()

        sut = Worker()
    }

    override func tearDown() {
        sut = nil

        super.tearDown()
    }

    // 2
    func test_givenPerformWorkIsCalled_whenUsingDefaultCallbackQueue_thenCallsBackOnMainQueue() {
        let expectation = expectation(description: "completion called")

        sut.performWork { result in
            XCTAssertTrue(Thread.isMainThread)
            XCTAssertEqual(try? result.get(), "Done")

            expectation.fulfill()
        }

        waitForExpectations(timeout: 5)
    }

    // 3
    func test_givenPerformWorkIsCalled_whenSpecifyingCallbackQueue_thenCallsBackOnThatQueue() {
        let callbackQueue = DispatchQueue(label: "com.williamboles.test-callback")
        let callbackQueueKey = DispatchSpecificKey<Bool>()
        callbackQueue.setSpecific(key: callbackQueueKey, value: true)

        let expectation = expectation(description: "completion called")

        sut.performWork(callbackQueue: callbackQueue) { result in
            XCTAssertTrue(DispatchQueue.getSpecific(key: callbackQueueKey) == true)
            XCTAssertEqual(try? result.get(), "Done")

            expectation.fulfill()
        }

        waitForExpectations(timeout: 5)
    }
}
```

1. `sut` ([system under test](https://en.wikipedia.org/wiki/System%5Funder%5Ftest?ref=williamboles.com)) is created fresh in `setUp` and torn down after each test, ensuring a clean state for every test run.
2. This test verifies the default behaviour - that when no `callbackQueue` is specified, the completion fires on the main queue. `Thread.isMainThread` is a straightforward check here since the main queue always maps to the main thread.
3. This test verifies the override path - that when a custom `callbackQueue` is specified, the completion fires on that queue. Since there's no `DispatchQueue.current` API to know which queue the code is currently being run on, we tag the queue with a [DispatchSpecificKey](https://developer.apple.com/documentation/dispatch/dispatchspecifickey?ref=williamboles.com) and check for it inside the callback to confirm we're on the right queue - a boolean value of `true` is chosen to make the assert straightforward but the real work is done with the key matching.

## Wrapping Up

By adding a `callbackQueue` parameter to our asynchronous methods, we shift the queue-jumping responsibility from every call-site to a single, predictable location inside the callee. Most callers get the sensible default of `.main` for free, and the few that need something different can override it explicitly. The scattered `DispatchQueue.main` code blocks that had faded into the background are replaced by a single, intentional decision at the source.

To see the complete working example, visit the [repository](https://github.com/wibosco/CapturingTheCallbackQueue-Example?ref=williamboles.com) and clone the project.