skip to content

In Dart, when main() calls an async function without awaiting it, which of that function's statements run before main's next line?

level: juniorimportance: should knowfreq 46%

answer

  1. not a deferred job
  2. synchronous until the first await
  3. await returns an uncompleted Future
  4. even await 42 suspends
  5. errors land in the returned Future

basics

~20 s

Every statement before the first await runs immediately, inside the call. At the first await the function suspends, returns an uncompleted Future, and main continues; the rest of the body resumes later from the event loop.

solid answer

~40 s

A Dart `async` function is not deferred: calling it executes its body synchronously up to the first `await`. There it registers the rest of the body as a continuation, returns an uncompleted `Future` to the caller, and the caller's next statement runs. The tail resumes only after the caller's synchronous code has finished: in a microtask if the awaited value was already available (even `await 42` suspends), or when the awaited future completes otherwise. So if `load()` prints `A`, awaits, then prints `B`, then `load(); print('C');` outputs A, C, B. Errors thrown anywhere in the body, even before the first `await`, complete the returned future instead of throwing to the caller.

code

dart · 17 lines
dart
Future<void> load() async {
  print('A');
  await Future<void>.delayed(Duration.zero);
  print('B');
}

Future<void> loadLate() async {
  await Future<void>.delayed(Duration.zero);
  print('A2');
}

void main() {
  load();
  loadLate();
  print('C');
}
// Output: A, C, B, A2

go deeper

for a junior

Recall that the body runs synchronously up to the first await, and that await hands control back to the caller with an uncompleted Future.

for a middle

Explain where the tail resumes: as a microtask for completed values, or when the awaited future completes, and why even await 42 lets the caller finish first.

for a senior

Show the practical traps: side effects before the first await run on the caller's turn, and errors never throw synchronously, so try/catch around an un-awaited call is dead code.

for a principal

Treat the pre-await prefix as part of an API's contract: keep it cheap and side-effect-light so callers can reason about ordering without reading the body.

## The rule: synchronous up to the first `await` A Dart function marked `async` is **not** a deferred job. Calling it starts executing its body **right away, synchronously, inside the caller's current code**, exactly like calling any other function. The body keeps running until one of two things happens: - it reaches its first `await` expression, or - it returns (or throws) without ever reaching one. At the first `await`, the function **suspends**: it registers "the rest of my body" as a continuation on the awaited value, hands the caller a `Future` that is not yet complete, and returns. The caller's next statement then runs. The remainder of the async body resumes later, driven by the isolate's **event loop** — the single loop per isolate that runs queued callbacks one at a time. The dart.dev async guide states it directly: an `async` function runs synchronously until the first `await` keyword, and all synchronous code before that keyword executes immediately. ## Walking through an example ```dart Future<void> load() async { print('A'); // runs inside the call await Future<void>.delayed(Duration.zero); // suspends here print('B'); // runs later, from the event loop } void main() { load(); // not awaited print('C'); } // Output: A, C, B ``` 1. `main` calls `load()`. `print('A')` runs immediately, still inside that call. 2. `load` reaches `await`, suspends, and returns an uncompleted `Future<void>` to `main`. 3. `main` continues and prints `C`, then returns. 4. Later, the zero-duration timer behind `Future.delayed` fires as an event, the awaited future completes, and `load` resumes to print `B`. Move `print('A')` below the `await` and the output becomes C, A, B, because nothing in `load` runs before `main` finishes its own synchronous code. ## What `await` does at the suspension point `await` **always** suspends, whatever it is given. What differs is when the continuation is scheduled: | Awaited value | When the rest of the body resumes | |---|---| | A future that is still pending | When that future completes, as part of completing it | | A future that has already completed | In a **microtask**, after the current synchronous code finishes | | A plain value, such as `await 42` | In a **microtask**, the same as an already-completed future | A **microtask** is a short callback on the isolate's microtask queue; the event loop runs all pending microtasks before it takes the next event (a timer, I/O completion or message). So even `await 42` lets the caller's remaining synchronous code run first, though it resumes before any timer. ## Consequences you meet in practice - **Side effects before the first `await` happen on the caller's turn.** Logging, setting a loading flag, or validating arguments at the top of an async function all run before the caller's next line. - **Errors never escape synchronously.** An `async` function catches anything its body throws, before or after the first `await`, and completes its returned future with that error. A `try`/`catch` around an un-awaited call catches nothing; you need to `await` the call or handle the future's error. - **Not awaiting a call does not stop it from starting.** The body up to the first `await` has already run; only the tail is left to the event loop. The `unawaited_futures` lint exists because silently dropping that future also drops its errors. - **Order inside one async function is still sequential.** Code after an `await` never runs before the awaited value is available; asynchrony only changes what the *caller* can do meanwhile. - **There is no background thread.** The prefix, the caller's code and the resumed tail all run on the same isolate's single thread, one after another. ## Where this shows up in Flutter code In a Flutter `onPressed` handler that calls an async method, everything before that method's first `await` runs inside the tap handler itself, on the UI isolate, before the handler returns. That is why setting a "loading" flag at the top of the method takes effect immediately, and why an expensive computation placed before the first `await` still delays the handler even though the method is marked `async`. Marking a function `async` changes how its result is delivered; it does not move the prefix of its body anywhere else. The same reasoning explains a common test surprise: a test that calls an async function without awaiting it can already observe the side effects of the pre-await prefix, but not of anything after it. ## Summary Think of an async function as two parts split at each `await`: the part before the first `await` is ordinary synchronous code executed by the call itself; every part after an `await` is a callback that the event loop runs later. Predicting output in interviews comes down to finding that first `await` and remembering that `await` suspends even when there is nothing to wait for.

  • Does an exception thrown before the first await reach the caller synchronously?
    No. An `async` function catches everything its body throws, before or after the first `await`, and completes its returned `Future` with that error. A `try`/`catch` around an un-awaited call therefore catches nothing; the caller has to `await` the call or attach an error handler to the returned future.
  • If the function awaits a value that is already available, such as await 42, does its tail run before the caller's next line?
    No. `await` always suspends. On an already-completed future or a plain value the continuation is scheduled as a microtask, so it runs after the caller's current synchronous code returns, but before any timer or I/O event that is waiting.

saying these in an interview costs you the question

  • Calling an async function schedules its whole body to run later.
  • Nothing in an async function runs until the caller awaits it.
  • Awaiting an already-completed future continues without suspending.
  • The code after await runs on a background thread.
  • An exception before the first await is thrown synchronously to the caller.