A Flutter library-loan screen awaits a renewal call and then shows a SnackBar through ScaffoldMessenger.of(context); what can go wrong, and how do you write it safely?
answer
- the user can leave during the await
- an unmounted context is invalid
- context.mounted since Flutter 3.7
- capture the messenger before the gap
- use_build_context_synchronously lint
basics
~20 sDuring the await the user may pop the screen, unmounting the element; using its context afterwards then trips assertions or acts on a dead position. Check context.mounted after the await, or capture ScaffoldMessenger.of(context) before it.
solid answer
~40 sAn `await` is an asynchronous gap: the handler resumes later, and by then the user may have left the loan screen, so the element behind `context` is unmounted and will never be mounted again. Looking up an ancestor through it then fails in debug builds with "Looking up a deactivated widget's ancestor is unsafe.", and in a `State`, even reading `context` asserts. There are two safe shapes. Check `if (!context.mounted) return;` right after the `await`; `BuildContext.mounted` has existed since Flutter 3.7, so it works in a `StatelessWidget` callback too. Or capture what you need before the gap, `final messenger = ScaffoldMessenger.of(context);`, because the root messenger from `MaterialApp` outlives the screen and can still show the snackbar. Capturing is wrong for screen-bound actions such as popping a `Navigator`. The `use_build_context_synchronously` lint flags unguarded uses.
code
dart · 31 linesimport 'package:flutter/material.dart';
abstract interface class LoanService {
Future<DateTime> renew(String loanId);
}
class RenewTile extends StatelessWidget {
const RenewTile({super.key, required this.loanId, required this.loans});
final String loanId;
final LoanService loans;
Future<void> _renew(BuildContext context) async {
final due = await loans.renew(loanId); // asynchronous gap
if (!context.mounted) return; // the user may have left the screen
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Renewed until ${due.toLocal()}')),
);
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text('Loan $loanId'),
trailing: TextButton(
onPressed: () => _renew(context),
child: const Text('Renew'),
),
);
}
}go deeper
Recall that after an await you check context.mounted before using the context again.
Explain why the element can be unmounted during the gap, what State.mounted and context.mounted each guard, and what the lint flags.
Choose deliberately between a mounted guard and capturing an app-level object before the gap, and reject captured navigators that can act on the wrong route.
Move asynchronous work and its user feedback out of widget callbacks into a layer that outlives screens, so context-after-await bugs cannot arise by design.
## The scenario The loan screen's renew button runs an `async` handler: it awaits `loans.renew(id)` and then shows 'Renewed until ...' in a snackbar through `ScaffoldMessenger.of(context)`. On a slow network the user taps back while the request is in flight. When the future completes, the handler resumes with a `context` whose element has been unmounted. ## What actually goes wrong - The element is **unmounted**; the framework documents that once unmounted, a context never becomes mounted again, and that using it while `mounted` is false triggers assertions. - An ancestor lookup such as `ScaffoldMessenger.of(context)` fails in debug builds with "Looking up a deactivated widget's ancestor is unsafe." - Inside a `State`, the `context` getter itself asserts: 'This widget has been unmounted, so the State no longer has a context'. - In release builds the assertions are gone, so the code may act on stale objects instead of failing clearly. The `BuildContext` documentation addresses this directly: if a context is used across an asynchronous gap, check `mounted` before interacting with it. ## Fix 1: check context.mounted after the gap ```dart final renewed = await loans.renew(loanId); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Renewed until ${renewed.due}')), ); ``` `BuildContext.mounted` was added in **Flutter 3.7**. Before that only `State.mounted` existed, so `StatelessWidget` callbacks had no direct guard. Skipping the snackbar when the screen is gone is usually the right product behaviour for screen-bound feedback. ## Fix 2: capture before the gap ```dart final messenger = ScaffoldMessenger.of(context); final renewed = await loans.renew(loanId); messenger.showSnackBar(SnackBar(content: Text('Renewed until ${renewed.due}'))); ``` This works because `MaterialApp` inserts a root `ScaffoldMessenger` above all routes; the captured state outlives the loan screen and shows the snackbar on whichever `Scaffold` is current. It is a deliberate choice: the user still learns that the renewal succeeded after navigating away. ## Choosing between them | Action after the await | Safe pattern | |---|---| | Snackbar that should still appear after leaving | Capture `ScaffoldMessenger.of(context)` before the gap | | Feedback that only makes sense on this screen | `if (!context.mounted) return;` | | `Navigator.of(context).pop()` or push from this screen | `context.mounted` check; never a captured navigator | | `setState` in a `State` | `if (!mounted) return;` | Capturing a `NavigatorState` before the gap is the dangerous variant: after the await, `pop` removes whatever route is on top, which may no longer be the loan screen. ## State.mounted versus context.mounted 1. In a `State`, `mounted` tells you whether **this State's** element is attached, which guards `this.context` and `setState`. 2. A context received as a parameter, for example the context a dialog's builder provides, belongs to a **different** element with its own lifetime. Guard it with that context's own `mounted`. 3. The `use_build_context_synchronously` lint flags a context used after an `await` without such a guard; treat its warnings as bugs, not style. ## Review checklist - Every `await` in a handler that later touches `context` has a `mounted` guard or a deliberate capture before it. - No `BuildContext` is stored in a field or passed to a service for later use. - Captures are limited to app-level objects that should outlive the screen.
- When is capturing an object before the await the wrong fix?When the action is bound to this screen. A `NavigatorState` captured before the gap still works after it, so `pop()` removes whatever route is now on top, possibly one the user just opened. For navigation and screen-specific feedback, check `context.mounted` after the await and skip the action if the screen is gone.
- Why is State.mounted not enough when a method also receives a dialog's BuildContext?`State.mounted` only reports whether this State's own element is attached. The dialog's context belongs to another element, the dialog route's content, which can be unmounted while the screen's State is still alive. After an await, check `dialogContext.mounted` before using that context, and `mounted` before `setState`.
saying these in an interview costs you the question
- Awaiting in onPressed is safe because the screen waits for the call
- A try/catch around the snackbar call fixes the context problem
- State.mounted also guards any other context passed into the method
- context.mounted exists only inside StatefulWidget code
- Capture Navigator.of(context) before the await to pop safely later