skip to content

In provider 6, what does context.watch<SurveyAnswers?>() return when no matching provider is found, and when is a nullable lookup appropriate?

level: middleimportance: nice to knowfreq 18%

answer

  1. provider 6.0.0 change
  2. nullable type argument
  3. null instead of throwing
  4. ProviderNullException for a null value
  5. Model and Model? resolve together

basics

~20 s

Since provider 6.0.0, a nullable type argument such as watch<SurveyAnswers?>() or read<SurveyAnswers?>() returns null when no provider is found, instead of throwing ProviderNotFoundException. Use it for reusable widgets that genuinely work with or without the provider.

solid answer

~40 s

In provider 6 the nullability of the type argument decides what a failed lookup does. `context.watch<SurveyAnswers>()` with no provider above throws `ProviderNotFoundException`; `context.watch<SurveyAnswers?>()` returns `null`, and `read` and `select` behave the same way. A related error is `ProviderNullException`: reading a non-nullable `T` from a provider whose value is `null`. 6.0.0 also made `Provider<Model>` and `Provider<Model?>` resolve to the same, deepest provider, so reading `Model?` can no longer miss a `Model` provider. Nullable lookups suit widgets that are optional consumers, like a progress bar that shows survey progress only inside a survey flow. They are the wrong fix for a misplaced provider: they turn a loud error into a silently empty UI.

code

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

// SurveyAnswers is a ChangeNotifier with a double progress getter.

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

  @override
  Widget build(BuildContext context) {
    // null outside a survey flow instead of ProviderNotFoundException
    final answers = context.watch<SurveyAnswers?>();
    if (answers == null) return const SizedBox.shrink();
    return LinearProgressIndicator(value: answers.progress);
  }
}

go deeper

for a junior

Recall that adding ? to the type in watch or read returns null instead of throwing when no provider is found.

for a middle

Explain the difference between ProviderNotFoundException and ProviderNullException, and the 6.0.0 change that unified Model and Model? lookups.

for a senior

Show judgement about when a nullable lookup is a designed state versus a masked placement bug, and push back on blanket nullable reads in review.

for a principal

Set a rule for shared widget libraries: optional provider dependencies must be documented and have a designed null state, keeping required dependencies loud.

## Two outcomes, chosen by the type argument When a provider read cannot find a match, provider 6 looks at whether the requested type is **nullable**: | Call | No provider above | Provider above with a null value | |---|---|---| | `context.watch<SurveyAnswers>()` | throws `ProviderNotFoundException` | throws `ProviderNullException` | | `context.watch<SurveyAnswers?>()` | returns `null` | returns `null` | | `context.read<SurveyAnswers?>()` | returns `null` | returns `null` | | `context.select<SurveyAnswers?, int?>((a) => a?.answered)` | selector receives `null` | selector receives `null` | The package describes this as 'optionally depending on a provider': reusable widgets that may be used in various locations, including outside of a provider. ## The two exceptions - **`ProviderNotFoundException`**: no provider of the type was found above the context. Its debug message starts 'Could not find the correct Provider<T> above this X Widget' and lists the usual causes: a hot reload after adding a provider in `main`, a provider in a different route, and a context that is an ancestor of the provider. - **`ProviderNullException`**: a provider was found, but its value is `null` and the requested type was non-nullable. The message reads 'The widget X tried to read Provider<T> but the matching provider returned null' and suggests asking for `T?` instead. ## What 6.0.0 changed Before provider 6, `Provider<Model>` and `Provider<Model?>` were different lookup keys, so defining a provider as `Model?` and reading it as `Model` threw `ProviderNotFoundException`. The 6.0.0 changelog made two breaking changes: 1. Providers that differ **only** by nullability are treated as the same; a lookup resolves to the **deepest** one. 2. A nullable type argument returns `null` when nothing is found, instead of throwing. Together they mean the type argument's `?` controls **failure behaviour**, not which provider is found. ## When a nullable lookup is right - A **reusable widget** that adds something when a provider is present: a `SurveyProgressBar` that renders progress inside a survey flow and nothing elsewhere. - A widget shared between an app and a **showcase or test harness** where the provider is deliberately absent. - **Optional features** behind a provider that some builds omit. In each case, the `null` branch is a real, designed state of the widget. ## When it is the wrong fix - Silencing `ProviderNotFoundException` on a pushed route or dialog. The real problem is placement; a nullable read turns a clear error into a screen that silently shows nothing. - Hiding a provider that is null because of a bug upstream. Prefer letting `ProviderNullException` point at it. - Everywhere 'just in case'. It pushes null checks into every reader and makes a missing provider indistinguishable from a legitimately absent one. ## Testing both branches A widget with an optional provider dependency has two behaviours, and a widget test should cover both: 1. pump the widget **without** a provider and expect the empty or fallback rendering; 2. pump it under `ChangeNotifierProvider<SurveyAnswers>.value(value: fakeAnswers, child: ...)` and expect the real rendering; 3. change the fake and call `notifyListeners()`, then pump, to confirm the `watch` still rebuilds. The first test is exactly what a non-nullable lookup would turn into a `ProviderNotFoundException`, which is a useful way to explain the difference: the nullable version makes 'no provider' a supported configuration rather than a crash. ## Reading the code A nullable lookup should make its null branch visible, for example with a pattern or an early return: ```dart final answers = context.watch<SurveyAnswers?>(); if (answers == null) return const SizedBox.shrink(); return LinearProgressIndicator(value: answers.progress); ``` If reviewers cannot say what the null branch means for the user, the lookup probably should not be nullable.

  • In provider 6, a Provider<Model?> sits above a widget that calls context.watch<Model>(); what happens?
    The lookup finds that provider, because providers differing only by nullability resolve to the same, deepest one. If its value is non-null the call returns it; if the value is `null`, the non-nullable read throws `ProviderNullException`, whose message suggests reading `Model?` instead.
  • Why is watch<T?>() a poor fix for ProviderNotFoundException on a pushed route?
    The provider really is missing on that route, so the widget would always get `null` there and render its empty branch. The user sees a blank or broken page with no error. Fix the placement: lift the provider or re-provide the instance with a `.value` constructor.

saying these in an interview costs you the question

  • watch<T?>() still throws ProviderNotFoundException when nothing is found
  • Provider<Model> and Provider<Model?> are separate lookups in provider 6
  • Making every lookup nullable is a safe default
  • ProviderNullException means no provider exists above the widget
  • A nullable lookup is the standard fix for a provider missing on a new route