skip to content

In provider 6, why do FutureProvider and StreamProvider require initialData, and how do they handle errors from the Future or Stream?

level: middleimportance: should knowfreq 32%

answer

  1. a value of T before completion
  2. required since the null-safe 5.0
  3. nullable T when nothing sensible exists
  4. catchError builds a fallback value
  5. no catchError: reported, not thrown

basics

~20 s

Descendants read a plain T, so FutureProvider and StreamProvider need initialData to expose until the first result arrives; it became required with null safety in provider 5.0. Errors go to catchError, which builds a fallback value, or are reported through FlutterError.reportError.

solid answer

~40 s

The package's async providers expose a plain `T`, not a snapshot, so readers need a value from the first frame. Since provider 5.0 (the null-safety release) `initialData` is required: `FutureProvider<List<String>>(create: ..., initialData: const [])` shows an empty list until today's specials load. When no sensible placeholder exists, make the type nullable — `FutureProvider<Menu?>(initialData: null)` — and read it as `Menu?`. Errors are not surfaced as a state: an optional `catchError` receives the context and the error and returns a fallback `T` to expose; without it, the provider reports the error with `FlutterError.reportError` and keeps the last value. `StreamProvider` also skips rebuilds when a new event is `==` to the previous one unless you pass `updateShouldNotify`. For loading and error states the UI must render, a `ChangeNotifier` that models them explicitly is usually clearer.

code

dart · 32 lines
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Menu {
  const Menu(this.items);
  final List<String> items;
}

Future<Menu> fetchMenu() async {
  await Future<void>.delayed(const Duration(milliseconds: 300));
  return const Menu(['Baguette', 'Pain au chocolat']);
}

class MenuScreen extends StatelessWidget {
  const MenuScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return FutureProvider<Menu?>(
      create: (_) => fetchMenu(),
      initialData: null, // no sensible placeholder, so the type is nullable
      catchError: (context, error) => const Menu([]), // fallback value
      child: Builder(
        builder: (context) {
          final menu = context.watch<Menu?>();
          if (menu == null) return const CircularProgressIndicator();
          return Text(menu.items.join(', '));
        },
      ),
    );
  }
}

go deeper

for a junior

Recall that FutureProvider and StreamProvider give widgets a plain value and need an initialData to show until the first result.

for a middle

Explain why initialData became required with null safety, how nullable types model 'not loaded', and how catchError and the default error report behave.

for a senior

Judge when the plain-value model is enough and when loading and error states must be explicit in a notifier, and guard streams with catchError.

for a principal

Decide how async state is modelled across the app so screens handle loading, empty and failure consistently rather than per-provider placeholders.

## What the async providers expose `FutureProvider<T>` and `StreamProvider<T>` in the provider package subscribe to a `Future` or a `Stream` and expose **a plain `T`** to descendants — not a wrapper with loading and error flags. A widget that reads `List<String>` gets a list, full stop. That design has two consequences, which are the whole question. ## Why `initialData` is required A widget can read the provider before the future completes or the stream emits. The provider must therefore have some `T` to hand out immediately. Before null safety it silently exposed `null`; the **5.0 migration** made `initialData` a required parameter on both classes, so the placeholder is explicit and type-checked. | Situation | Declaration | What readers see first | |---|---|---| | an empty collection is a valid start | `FutureProvider<List<String>>(..., initialData: const [])` | an empty list | | a neutral value exists | `StreamProvider<int>(..., initialData: 0)` | `0` | | no sensible placeholder | `FutureProvider<Menu?>(..., initialData: null)` | `null`, read as `Menu?` | With a nullable type, readers must ask for the nullable type too (`Menu?`); provider 6 resolves `Menu` and `Menu?` lookups to the same provider, but the value may still be `null`. For a `StreamProvider`, `initialData` is used only while the provider has no value yet. ## How errors are handled Because readers get a plain `T`, there is no error state to switch on. Instead, both classes take an optional **`catchError`**, typed as `ErrorBuilder<T>` — `T Function(BuildContext context, Object? error)`: - **with `catchError`**, the provider calls it and exposes the value it returns, such as an empty menu or a cached copy; - **without it**, the provider reports the error through `FlutterError.reportError`, with a message saying no `catchError` was provided, and exposes nothing new, so readers keep the previous value (possibly still `initialData`). The `StreamProvider` documentation calls it an error to pass a stream that can emit errors without a `catchError`. The error is not rethrown to readers. ## Update filtering By default `StreamProvider` assumes the stream carries immutable data and **does not rebuild dependents** when the new event is `==` to the previous one. A stream that emits the same mutable object after changing it would therefore never update the UI; pass `updateShouldNotify` or emit new objects. ## Laziness and `.value` The `create` variants are lazy like other providers: the future or stream is not requested until the value is first read. `FutureProvider.value` and `StreamProvider.value` wrap an existing future or stream and start listening when the provider builds. ## When to pick something else The package describes `StreamProvider` as a way to expose a stream's content to many widgets — a battery level, a query result — and says replacing `ChangeNotifier` with streams is outside its scope. In practice: 1. Use `FutureProvider` for a one-shot, fire-and-forget value with a harmless placeholder, such as today's specials on a bakery home screen. 2. Use `StreamProvider` for a continuous feed, such as oven temperature readings. 3. When the screen must show a spinner, an error message and a retry button, model those states in a `ChangeNotifier` exposed with `ChangeNotifierProvider`, because the async providers cannot distinguish "still loading" from "loaded but empty" or "failed". ## Testing async providers The `.value` constructors make these providers easy to drive in widget tests. A test can wrap the screen in `FutureProvider<Menu?>.value(value: Future.value(fakeMenu), initialData: null, child: ...)`, or feed a `StreamController`'s stream into `StreamProvider.value` and add events from the test, pumping a frame after each. Because the providers expose plain values, the assertions stay simple: after the first pump the widget shows the placeholder, after the future completes and another frame is pumped it shows the data. ## Common mistakes - Choosing `initialData: const []` and then treating an empty list as "no data yet" when the real answer might also be empty. - Omitting `catchError` on a stream that can fail, and wondering why the error only shows up in logs. - Emitting the same mutable object on a stream and expecting rebuilds.

  • How is provider's FutureProvider different from Riverpod's class of the same name?
    Provider's `FutureProvider` exposes a plain `T`, needs `initialData`, and handles errors only through `catchError` or a logged report. Riverpod's `FutureProvider` exposes an `AsyncValue<T>` with explicit loading, data and error cases. Same name, different model — worth saying explicitly in an interview so the two are not conflated.
  • What happens when a StreamProvider's stream emits an event equal to the previous one?
    Dependents are not rebuilt. The class assumes immutable data and compares new and previous values with `==` unless you pass `updateShouldNotify`. That is efficient for immutable events but hides updates if the stream re-emits the same mutable object after changing it.

saying these in an interview costs you the question

  • FutureProvider exposes an AsyncSnapshot with loading and error flags.
  • initialData is optional in provider 6 and defaults to null.
  • Without catchError, the error is rethrown from every read of the provider.
  • catchError is also used as the value shown while the future is pending.
  • StreamProvider rebuilds dependents for every event, even equal ones.