In provider 6, why do FutureProvider and StreamProvider require initialData, and how do they handle errors from the Future or Stream?
answer
- a value of T before completion
- required since the null-safe 5.0
- nullable T when nothing sensible exists
- catchError builds a fallback value
- no catchError: reported, not thrown
basics
~20 sDescendants 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 sThe 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 linesimport '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
Recall that FutureProvider and StreamProvider give widgets a plain value and need an initialData to show until the first result.
Explain why initialData became required with null safety, how nullable types model 'not loaded', and how catchError and the default error report behave.
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.
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.