Blocks are scheduled on the main queue from the main thread, and one of them schedules another. In what order do the prints appear?

27.3.1 · L2 · Interview

Predict first

What is the order of the printed lines?

import Foundation

print("1 start")
DispatchQueue.main.async {
    print("2 outer begins")
    DispatchQueue.main.async { print("5 inner") }
    print("3 outer ends")
}
DispatchQueue.main.async { print("4 sibling") }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { print("6 done"); exit(0) }
print("1b still in main code")
dispatchMain()
  • 1 start, 1b, 2, 3, 4, 5, 6
  • 1 start, 2, 3, 5, 4, 1b, 6
  • 1 start, 1b, 2, 5, 3, 4, 6

Mental model. A single till with a line of customers. Someone who joins while being served goes to the back of the line, not in front of the person already waiting.

DispatchQueue.main.async never runs its block inline: it appends the block to the main queue and returns at once. The main queue is first in first out and runs one block at a time, and the code that is already running (here the top-level script) counts as the current block, so it finishes first. That gives: “1 start”, then everything the current code does synchronously (“1b”), and only then the queued blocks in the order they were enqueued: the outer block (prints 2 and 3), then the sibling (4). The inner block was enqueued while the outer block was running, so it went to the back of the queue, behind the sibling that was already waiting, and prints last (5).

Two consequences are worth saying out loud. A nested async is a way of saying “after everything already queued”, which is a legitimate trick to run something after the current layout or event pass (but a smell if you rely on it for correctness). And the rule is only about the queue’s order: the timer block runs after 0.1 s, and anything that depends on timing across different queues is not ordered at all. If you need the inner block immediately, call the code directly; if you are already on main and call main.sync, libdispatch detects the self-deadlock and traps (Q218).

Async appends and returns; the inner block lands behind the already-queued sibling.

Common traps

  • Expecting a nested main.async to run immediately. It runs after every block already in the queue.
  • Treating async-on-main as “next line”. The rest of the current block always finishes first.

They might follow up with

  • What changes if the first block was main.sync from a background thread? The background thread waits, but the main queue order is still first in first out.
  • How does MainActor differ? Isolation, not a queue, and a hop uses the same main executor, but the ordering rules for unstructured tasks are weaker (see the task-ordering question).

Related questions

  • 27.3.2 On a serial queue, which prints are guaranteed to be ordered, what does a barrier guarantee, and what happens…
  • 27.3.3 A screen stores a DispatchWorkItem whose closure uses self. Does cancelling the item fix the leak, and what…
  • 27.4.2 In what order do these prints appear when a main-actor function creates a Task, yields, and starts a detached…
  • 6.3.1 What is a run loop, and what happens in one iteration?
  • 6.3.3 What is the main-thread contract, and why does DispatchQueue.main.sync from the main thread crash?
  • 10.1.1 What is the difference between serial and concurrent queues and between sync and async, and what do QoS and…

On a serial queue, which prints are guaranteed to be ordered, what… →