skip to content

In Dart's package:test, how do you assert that a call throws or that a Future completes or fails, and when do you need expectLater?

level: middleimportance: must knowfreq 50%

answer

  1. wrap a synchronous throw in a closure
  2. throwsA with isA<T>()
  3. completion wraps the value matcher
  4. expect returns before an async match
  5. await expectLater to sequence

basics

~10 s

Wrap a throwing call in a closure: expect(() => parse('x'), throwsFormatException). For Futures use completion(matcher) or throwsA(isA<T>()); expect returns before those async matches finish, so await expectLater when later code depends on the result.

solid answer

~40 s

`throwsA(matcher)` matches a zero-argument function that throws, a `Future` that completes with an error, or a function returning such a `Future`. So a synchronous failure must be wrapped: `expect(() => parser.parse('2026-13-01'), throwsFormatException)`; calling `parse` directly throws before `expect` even runs. Shortcuts exist, such as `throwsFormatException` and `throwsArgumentError`, and `throwsA(isA<FormatException>().having((e) => e.message, 'message', contains('month')))` checks details. For success, `completion(equals(date))` matches a `Future`'s value and `completes` just checks it completes. These matchers are **asynchronous**: `expect` returns immediately and the test waits for the match before finishing, but the next line runs first. When later steps depend on it, `await expectLater(future, completion(...))`, which returns a `Future`. An `async` test body that simply `await`s the value and uses a plain `expect` is often clearer.

code

dart · 21 lines
dart
import 'package:test/test.dart';

/// Strict: DateTime.parse alone would overflow 2026-13-01 into 2027-01-01.
DateTime parseIsoDate(String s) {
  final d = DateTime.parse(s); // FormatException if malformed
  if (d.month != int.parse(s.substring(5, 7))) {
    throw FormatException('month or day out of range', s);
  }
  return d;
}

void main() {
  test('rejects month 13', () {
    expect(() => parseIsoDate('2026-13-01'), throwsFormatException);
  });

  test('async lookup fails for unknown region', () async {
    Future<DateTime> lookup() async => throw ArgumentError('region');
    await expectLater(lookup(), throwsA(isA<ArgumentError>()));
  });
}

go deeper

for a junior

Recall the closure rule for throwing calls and the throwsFormatException-style shortcuts.

for a middle

Explain completion, completes and throwsA on Futures, why expect returns before they finish, and when to await expectLater.

for a senior

Show you write failure assertions that pin the error type and message, and avoid late uncaught async errors that blame the wrong test.

for a principal

Set team conventions for async assertions so failure messages stay precise and suites stay deterministic as they grow.

## The library under test Suppose a date-parsing library exposes a synchronous, **strict** `parseIsoDate(String)` that throws `FormatException` on malformed or out-of-range input. (It needs its own check: `DateTime.parse` throws on malformed text but overflows out-of-range parts, reading `2020-01-42` as 2020-02-11.) It also has an asynchronous `HolidayCalendar.next(DateTime from)` that loads data and returns a `Future<DateTime>`. The test needs to cover both success and failure for both. ## Throwing, synchronously `throwsA(matcher)` is a matcher for **something that throws**. It accepts three kinds of `actual`: 1. a function with no arguments that throws when called; 2. a `Future` that completes with an error; 3. a function that returns a `Future` which completes with an error. The first case is where most mistakes happen: ```dart // Wrong: parseIsoDate throws while the arguments are evaluated, // before expect runs, so the test fails with the raw exception. expect(parseIsoDate('2026-13-01'), throwsFormatException); // Right: pass a closure, so the matcher calls it and catches the error. expect(() => parseIsoDate('2026-13-01'), throwsFormatException); ``` The matcher package ships shortcuts for common types: `throwsArgumentError`, `throwsFormatException`, `throwsRangeError`, `throwsStateError`, `throwsException`, `throwsUnimplementedError`. For anything else, or to check fields, combine `throwsA` with `isA<T>()` and `having`: ```dart expect( () => parseIsoDate('2026-02-30'), throwsA(isA<FormatException>() .having((e) => e.source, 'source', '2026-02-30')), ); ``` The bare `throws` matcher is deprecated; the package asks you to assert at least the error type with `throwsA`. ## Futures: completes, completion, throwsA | Matcher | Passes when the Future | |---|---| | `completes` | completes with any value | | `completion(m)` | completes with a value matching `m` | | `throwsA(m)` | completes with an error matching `m` | | `doesNotComplete` | never completes during the test | All of these are **asynchronous matchers**. With `expect`, the call returns immediately; package:test keeps the test open until the match succeeds or fails. That is enough when the assertion is the last thing the test does. It is not enough when the next line depends on the outcome, because that line runs first. ## expectLater `expectLater` is `expect` that **returns a `Future`** completing when the matcher finishes. `await` it to sequence steps: ```dart test('next holiday after Christmas is New Year', () async { final calendar = HolidayCalendar(source: fakeSource); await expectLater( calendar.next(DateTime(2026, 12, 26)), completion(DateTime(2027, 1, 1)), ); expect(fakeSource.requests, 1); // runs only after the match }); ``` If the matcher fails asynchronously, the failure is delivered through the returned `Future`, so an un-awaited `expectLater` can surface its failure late. ## The plain-await alternative For success cases, the simplest test is often: ```dart test('parses leap day', () async { final date = await calendar.resolve('2028-02-29'); expect(date.day, 29); }); ``` The runner waits for the `Future` returned by an `async` body. Use `completion`/`throwsA` when you want the matcher's failure message, or when asserting on a `Future` you must not await directly. ## Pitfalls - **Missing closure** for synchronous throws: the exception escapes before `expect`. - **Forgetting `async`/`await`** in the test body: the test can finish before the work it started, and a late error is reported as an uncaught async error that fails the test, sometimes a different one. - **Asserting only `throwsException`** when the code throws an `Error` subtype such as `ArgumentError`; `Error` is not an `Exception`, so the matcher fails. - **Sequencing on `expect`**: code after an async `expect` does not wait for it. ## Choosing the assertion | Situation | Write | |---|---| | Synchronous call must throw a known type | `expect(() => f(), throwsFormatException)` | | Synchronous call must throw with details | `expect(() => f(), throwsA(isA<T>().having(...)))` | | Future must succeed, value matters | `expect(await future, value)` or `completion(value)` | | Future must fail | `await expectLater(future, throwsA(isA<T>()))` | | Future must never complete in the test | `expect(future, doesNotComplete)` | | Later steps depend on the outcome | `await expectLater(...)` | ## What the runner does with late errors package:test runs each test in its own zone. An asynchronous error that nobody handles, for example a `Future` started without `await` that later fails, is reported as a failure of the test whose zone it belongs to. If it surfaces after that test finished, it can turn a passing result into a failure, or be missed if the suite has already completed. The remedy is always the same: await what you start, or attach a handler before the future completes with an error.

  • Why does expect(parseIsoDate('bad'), throwsFormatException) fail even though the function does throw?
    Dart evaluates arguments before calling `expect`, so `parseIsoDate('bad')` throws first and the exception propagates out of the test body. `throwsA` never sees it. Passing `() => parseIsoDate('bad')` lets the matcher call the function inside its own error handling.
  • When is plain expect with an async matcher enough, without expectLater?
    When the assertion is the last thing the test does and nothing afterwards depends on it. package:test keeps the test running until the async matcher has matched or failed, so the result is still reported. As soon as later code must run after the match, use `await expectLater`.
  • Why might throwsException fail for code that throws ArgumentError?
    `ArgumentError` extends `Error`, not `Exception`, and `throwsException` matches only `Exception` instances. Use `throwsArgumentError` or `throwsA(isA<ArgumentError>())` for it.

saying these in an interview costs you the question

  • Passes the call itself, not a closure, to a throws matcher
  • Believes expect waits for completion() before running the next line
  • Uses the deprecated bare throws matcher without checking the error type
  • Thinks ArgumentError matches throwsException
  • Omits async in a test body that starts Futures