In Dart, when main() calls an async function without awaiting it, which of that function's statements run before main's next line?
answer
- not a deferred job
- synchronous until the first await
- await returns an uncompleted Future
- even await 42 suspends
- errors land in the returned Future
basics
~20 sEvery 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 sA 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 linesFuture<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, A2go deeper
Recall that the body runs synchronously up to the first await, and that await hands control back to the caller with an uncompleted Future.
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.
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.
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.