Course outline

Dart / Contracts: patterns, generic APIs, and asynchronous results / Lesson 11

Own Future results, errors, and waiting semantics

Use async and await with deliberate sequencing, early error observation, and truthful timeout or cancellation expectations.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish an eventual result from a thread, an isolate, and an automatically cancellable operation.
  • Place error handling at the awaited operation and preserve owned completion or cleanup.

Async work still runs under an execution model

A Future represents an eventual value or error. async allows a function to return a Future and await eventual results, but it does not move CPU work to another isolate or make arbitrary code parallel. Work on one isolate shares its event-loop execution, so a long synchronous calculation can delay other events even inside an async function. Awaiting I/O can let the isolate process other work while the operation is pending. Separate those mechanisms before claiming responsiveness: asynchronous waiting, CPU parallelism, and durable background processing are different architectural choices.

Start and observe related operations deliberately

Awaiting one operation and then starting another enforces sequencing. Starting independent operations first and awaiting their combined results can allow overlap when their implementations actually perform asynchronous work. Future.wait associates results with input order, not completion order. The sample uses trivial fixed computations, so it demonstrates ownership of results rather than a speed improvement. Attach error handling promptly: starting a Future and leaving it unobserved while awaiting unrelated work can expose errors before the intended handler is attached. Review independence and resource bounds before replacing a sequential path with concurrent starts.

dart Observe two explicitly created Future results
import 'dart:async';
Future<int> countBatch(int count) async => count;
Future<void> main() async {
  final first = countBatch(2);
  final second = countBatch(3);
  final results = await Future.wait<int>(<Future<int>>[first, second]);
  print(results[0] + results[1]);
}
Expected output
5

Expected output is 5. The input order determines the positions in results. No I/O, timing experiment, isolate creation, or Dart execution occurs in this authored example.

Await inside the boundary that translates the error

A synchronous try around creation of an asynchronous task is not a universal handler for its eventual failure. Await the Future inside the intended try when using async syntax, or attach an appropriate Future error chain. The example catches a declared FormatException after awaiting the operation and prints a fixed safe message. A broader catch could conceal programming defects or mislabel an unrelated failure. Preserve useful diagnostic context separately without exposing sensitive payloads. Awaiting and catching does not undo an external side effect that completed before the failure was reported.

Make completion and cleanup visible

The finally marker in this program follows either a value path or a caught failure path, but it does not prove any real resource was closed. A Future<void> main allows the entry point to represent completion of its asynchronous work. Do not report success before a consequential Future has completed unless the product explicitly returns an accepted or pending state with an owned follow-up path. If the caller abandons the screen or request, decide who still owns the operation and its eventual error rather than letting it become an accidental detached task.

dart An awaited error with a narrow translation boundary
import 'dart:async';
Future<int> loadCount() async {
  throw const FormatException('synthetic invalid count');
}
Future<void> main() async {
  try {
    final count = await loadCount();
    print(count);
  } on FormatException {
    print('invalid count');
  } finally {
    print('finished');
  }
}
Expected output
invalid count
finished

Expected output is invalid count then finished. The thrown error is synthetic, no provider is contacted, and no compiler or runtime execution is claimed.

A Future timeout is not general cancellation

Future.timeout changes what the waiting Future reports after its time limit; it does not generally stop the underlying source operation. Future.wait also does not automatically roll back or cancel all sibling effects on failure. Cancellation requires support from the actual API, resource, or an explicit cooperative protocol. The unawaited helper can document that a Future is intentionally not awaited, but it does not consume its errors or manufacture ownership. State the outcome-query and retry rules for remote writes. This course provides reasoned source examples only and makes no scheduler, timing, network, cancellation, or reliability execution claim.

Common errors and fixes

An async function still blocks event processing during CPU work.

Why it happens
Asynchronous syntax was mistaken for a separate execution isolate.
How to fix it
Separate or bound the calculation and choose a supported isolate strategy when justified.
Prevent it next time
Classify work as synchronous computation or asynchronous waiting before optimizing.

An eventual Future error escapes the intended try.

Why it happens
The boundary created the task without awaiting or attaching error handling.
How to fix it
Observe the Future inside the correct error boundary promptly.
Prevent it next time
Require an owner for every consequential Future and its failure.

A timed-out write is retried as though it never happened.

Why it happens
Waiting timeout was confused with remote cancellation or rollback.
How to fix it
Use a supported outcome query or stable request-identity protocol.
Prevent it next time
Model uncertain outcomes explicitly before adding retries.

Check your understanding

1. Does adding unawaited to a fire-and-forget provider call make its errors safe?

Show worked solution

No. It documents intent for analysis but does not install an error handler, cancel work, or define who observes final completion. Give the operation an explicit owner and error policy, or await it within the callerโ€™s lifetime. If durable background completion is required, design durable work state rather than assuming an in-memory Future will survive process loss.

2. When should two batch requests be started before awaiting either result?

Show worked solution

When their business ordering and resource dependencies permit overlap and the application can promptly observe both successes and failures. Bound concurrency if the input grows. Starting many Futures is not a capacity policy, and a failed combined result does not erase effects already completed by another request. Keep the user-visible outcome and recovery behavior explicit.

Key takeaways

  • A Future represents eventual completion, not automatic parallelism or cancellation.
  • Every started asynchronous operation needs a result and error owner.

Official references