skip to content

In Dart, what does a function marked `async` return to its caller, and what does `await` do inside it?

level: juniorimportance: must knowfreq 82%

answer

  1. always a future, never the raw value
  2. return value completes it
  3. throw completes it with an error
  4. synchronous until the first await
  5. await suspends, then resumes or throws

basics

~20 s

An async Dart function always returns a Future<T> immediately; returning a value completes it and throwing completes it with an error. Inside, await suspends the function until a future completes, then resumes with its value or rethrows its error.

solid answer

~40 s

Marking a body `async` makes the function return a `Future<T>` no matter what the body does: `return 42;` in a `Future<int>` function completes that future with `42`, and a `throw` completes it with an error rather than throwing out of the call. The body runs **synchronously until its first `await`**; at that point the function suspends, the caller gets the pending future and carries on, and the function resumes later when the awaited future completes. `await` evaluates to the completed value, or rethrows the error so an ordinary `try`/`catch` around it works. A function with nothing to return should be declared `Future<void>` so callers can still await it, and `await` is only allowed inside an `async` body.

code

dart · 14 lines
dart
Future<String> greet(String name) async {
  if (name.isEmpty) throw ArgumentError('name is empty');
  await Future.delayed(const Duration(milliseconds: 100));
  return 'Hello, $name';
}

Future<void> main() async {
  print(await greet('Dash')); // Hello, Dash
  try {
    await greet('');
  } on ArgumentError catch (e) {
    print('caught: ${e.message}'); // caught: name is empty
  }
}

go deeper

for a junior

Say plainly that an async function returns a Future, that return completes it and throw fails it, and that await gives you the value or rethrows the error.

for a middle

Explain that the body runs synchronously until the first await, what the caller receives at that point, and why Future<void> beats void for async functions.

for a senior

Diagnose bugs where a Future leaked through as a value, a try without await caught nothing, or code before the first await ran earlier than expected.

for a principal

Set conventions for async APIs: Future<void> over void, no needless async wrappers, and lints that keep futures from silently escaping.

## What `async` changes about a function A `Future<T>` in Dart is an object representing a value of type `T` (or an error) that will be available later. Adding `async` before a function body changes the function's contract in three ways: 1. **The return value is always a future.** The declared type is `Future<T>`, and the caller receives that future immediately, before the work finishes. 2. **`return` completes the future.** Inside the body you return a plain `T`; Dart wraps it. Returning a `Future<T>` also works: the result follows that future. 3. **`throw` completes the future with an error.** An exception inside an `async` body never escapes the call synchronously, even if it happens before the first `await`. The caller sees it only when it awaits the future or attaches an error handler. ```dart Future<int> loadScore() async { final raw = await fetchRaw(); // suspends here if (raw.isEmpty) throw FormatException('empty'); return int.parse(raw); // completes the future } ``` ## When the body actually runs The dart.dev async guide states the rule precisely: **an `async` function runs synchronously until the first `await`**. Everything before that first `await` executes during the call itself, on the caller's stack. When execution reaches `await`, the function **suspends**: it hands the still-pending future back to the caller, and the caller continues with its next statement. Later, when the awaited future completes, the rest of the body resumes. ```dart Future<void> f() async { print('A'); await null; print('C'); } void main() { f(); print('B'); } // prints A, B, C ``` Nothing here involves another thread. A Dart isolate runs one piece of code at a time; `await` simply lets other pending work run while this function waits. ## What `await` evaluates to - If the awaited future completes with a **value**, the `await` expression evaluates to that value, fully typed: `await Future<int>` is an `int`. - If it completes with an **error**, `await` **rethrows** that error at that line, with its stack trace, so `try`/`catch`/`finally` around awaits behaves just like synchronous code. - Awaiting a non-future value is allowed but pointless; the `await_only_futures` lint (core set) flags it, with `await null` accepted as a deliberate yield. - `await` is legal only inside a function body marked `async` (or `async*`). ## Return types and common mistakes | Declaration | What the caller gets | Verdict | |---|---|---| | `Future<int> load() async` | a future completing with an `int` | normal | | `Future<void> save() async` | a future to await for completion and errors | normal for no-value work | | `void save() async` | nothing it can await | fire-and-forget only; `avoid_void_async` flags it | | `int load() async` | compile error | an `async` body cannot return a bare `int` type | Mistakes interviewers listen for: - **Forgetting `await`**: `final score = loadScore();` makes `score` a `Future<int>`, not an `int`. The analyzer usually catches it when the variable is used as an `int`, but not when it is only printed or passed along as an `Object`. - **Expecting a synchronous throw**: wrapping `loadScore()` in `try` without `await` catches nothing, because the error lands in the future. - **Adding `async` for no reason**: Effective Dart says not to use `async` when it has no useful effect, such as a function that only returns another future. ## `async` without `await` An `async` body that never awaits is legal, and sometimes it is the shortest correct code. Effective Dart lists the cases where `async` still earns its place: - **Returning an error asynchronously**: `async` plus `throw` is shorter than `return Future.error(...)`. - **Wrapping a plain value**: `Future<String> label() async => 'ready';` is shorter than `Future.value('ready')`. - **Using `await`**, the obvious case. The opposite advice also holds: if removing `async` does not change behaviour, for example a function that only returns another future, such as `return Future.any([left, right]);`, leave it off. ## Why this matters in practice Knowing exactly what an `async` function returns explains most day-to-day async bugs: a value that is still a `Future`, an error that was never awaited, a statement that ran earlier than expected because everything before the first `await` is synchronous. It is the foundation for combining futures with `Future.wait`, adding a `timeout`, or deciding when a future may deliberately go unawaited.

  • Why should an async function with no result return `Future<void>` rather than `void`?
    A `Future<void>` lets callers await completion and receive errors; a `void` async function gives them nothing to wait on, so its failures surface only as unhandled errors and ordering is lost. Effective Dart asks for `Future<void>`, and the opt-in `avoid_void_async` lint flags `void f() async`, with an exception for a top-level `main`.
  • If an async function throws before its first await, does the caller's try/catch around the call catch it?
    Only if the caller awaits. The exception completes the returned future with an error instead of propagating synchronously, so `try { load(); } catch (_) {}` without `await` catches nothing, while `try { await load(); } catch (_) {}` does.

saying these in an interview costs you the question

  • An async function returns its value directly once it finishes
  • await blocks the thread until the future completes
  • Code before the first await runs later, on the event loop
  • An exception before the first await is thrown synchronously to the caller
  • void is the right return type for an async function with no result