Home › Mobile › setState After Dispose: Guard Async Callbacks
Beginner 5 min · September 23, 2026

setState After Dispose: Guard Async Callbacks

Check mounted before setState: async callbacks can fire after dispose when users navigate away.

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Notes here come from systems that actually shipped.

Follow
✓ Production
production tested
September 26, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 11 min
  • ✓A Flutter app with at least one StatefulWidget
  • ✓Basics of async await and routes
  • ✓flutter test running in your project
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • setState called after dispose means an async callback fired after the State object was unmounted, usually because the user navigated away mid-request
  • Guard every async continuation with if (!mounted) return before calling setState, since the await gap lets disposal happen first
  • Cancel Timers, StreamSubscriptions, and AnimationControllers in dispose so their callbacks never reach a dead State
  • Prefer FutureBuilder, StreamBuilder, or a state-management layer that owns the subscription lifecycle instead of raw callbacks
✦ Definition~90s read
What is setState After Dispose?

Every StatefulWidget has a State object whose lifetime the framework manages: it calls initState when the widget enters the tree, build whenever it must render, and dispose when the widget leaves the tree for good — route popped, tab switched, parent rebuilt with a different widget type. After dispose, the State's element is unmounted: it has no position in the tree, no valid context, and no screen to update.

★
Imagine calling a restaurant to place an order, then moving to a new apartment before the food arrives.

Calling setState then is illegal because there is nothing to schedule a rebuild against, so Flutter throws setState() called after dispose().

The mounted property is your window into that lifetime. It is true from initState until dispose starts, then flips to false forever. Checking if (!mounted) return after an await is the canonical guard: if disposal happened during the gap, you silently drop the stale result instead of rebuilding a ghost.

Note that mounted is only valid on State — StatelessWidgets and external callbacks must arrange cancellation or ownership instead.

Three callback sources cause nearly every incident. Futures from network or database calls complete whenever the backend answers, regardless of navigation. Timers and periodic callbacks fire on wall-clock time, happily past disposal. Streams and animation controllers push events until explicitly canceled or disposed.

The fix has two halves: guard continuations with mounted, and cancel recurring sources in dispose. One half alone leaves holes — a mounted check cannot stop a leaking Timer from draining battery, and cancellation cannot cover a Future that is already in flight.

Plain-English First

Imagine calling a restaurant to place an order, then moving to a new apartment before the food arrives. The delivery driver shows up at your old address and knocks on an empty door — that failed delivery is setState after dispose. Your widget ordered some data, the user navigated to another screen while it was loading, and when the data finally arrived there was no screen left to update.

It is the most common async crash in Flutter apps: setState() called after dispose(). You fetch a profile, the user taps back before the response lands, and the continuation calls setState on a State object the framework already tore down. In debug builds you get a loud exception; in production the orphaned callback can leak timers, streams, and controllers that keep running against a dead screen.

The trap is the await gap. Between starting an async operation and its completion, the framework makes no promises: the user can pop the route, switch tabs, or hot-reload the tree. Any code after the await runs in a world that may no longer contain your widget. Developers who learned setState before async patterns sprinkle it after every fetch, and each one is a latent crash waiting for a slow network and an impatient user.

This guide makes the lifecycle click. You will see exactly when dispose runs, why mounted is the only safe guard, how to cancel every callback source in dispose, and which patterns — FutureBuilder, StreamBuilder, disciplined controllers — remove whole classes of the bug. By the end, async-after-dispose crashes will read as obvious lifecycle mistakes, not mysteries.

The Lifecycle Behind the Error: Mount, Build, Dispose

A State object lives only while its element sits in the widget tree. initState runs once on insertion, build runs whenever the framework needs pixels, and dispose runs once on permanent removal — popping the route, switching tabs with state disposal, or rebuilding a parent that swaps widget types. After dispose returns, the element unmounts and the State becomes a husk: its context is invalid, its ticker is dead, and setState has no scheduler to talk to.

The mounted flag mirrors this lifetime exactly. It turns true before initState and false as dispose begins, never to turn true again. That makes it the only reliable question to ask after an await: am I still in the tree? Code before the first await in an event handler runs synchronously while mounted is guaranteed, but every line after an await runs later, in a world where anything — including disposal — may have happened.

Internalize one rule and most incidents vanish: no setState after an await without a mounted check. Read your State files with that lens and the bugs glow. Event handlers that fetch-then-set, timer callbacks that tick-then-set, stream listeners that receive-then-set — each needs either the guard or a lifecycle owner that dies with the widget.

io/thecodeforge/flutter/lifecycle_guard.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import 'package:flutter/material.dart';

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key, required this.userId});
  final String userId;

  @override
  State<ProfileScreen> createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  String? _name;

  Future<void> _load() async {
    final name = await fetchName(widget.userId); // gap: user may leave
    if (!mounted) return; // widget gone: drop the stale result
    setState(() => _name = name);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text(_name ?? 'Loading...')));
  }
}

Future<String> fetchName(String id) async {
  await Future<void>.delayed(const Duration(seconds: 2));
  return 'Ada $id';
}
📊 Production Insight
A social app's profile screen crashed only on slow hotel Wi-Fi. The await gap stretched past users' patience, they tapped back, and the unguarded continuation did the rest.
🎯 Key Takeaway
mounted is true only between initState and dispose — check it after every await before touching setState.

The Async Gap: Why Navigation Beats Your Network Call

Dart awaits do not block the UI — that is the feature and the hazard. When your handler awaits a Future, the method suspends and control returns to the event loop, which keeps processing taps, including the back button. The route pops, dispose runs, the State unmounts. Seconds later the network answers, the method resumes exactly where it suspended, and setState fires into the void.

Slow networks widen the gap and fast users exploit it. A 200-millisecond response rarely loses the race; a 2-second p95 response loses constantly, especially on flows like search where users act on partial results. Debounced keystroke searches are the classic pile-up: five keystrokes launch overlapping requests, the user taps a result, and three stale continuations arrive after disposal. Each unguarded one throws.

Design for the loss, not the win. Assume every in-flight request will complete after disposal and make that outcome boring: guard with mounted, ignore stale responses by request id, or cancel the operation outright. The network owes you nothing — responses arrive when they arrive, and your widget may be long gone. Log dropped responses in debug builds so you can distinguish a cancelled race from a request that never fired.

io/thecodeforge/flutter/stale_search_guard.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import 'package:flutter/material.dart';

class SearchScreen extends StatefulWidget {
  const SearchScreen({super.key});

  @override
  State<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  List<String> _results = const [];
  int _requestId = 0;

  Future<void> _search(String query) async {
    final mine = ++_requestId; // tag this request
    final hits = await runSearch(query); // gap: user may navigate
    if (!mounted || mine != _requestId) return; // gone or stale: ignore
    setState(() => _results = hits);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: ListView(children: [for (final r in _results) Text(r)]));
  }
}

Future<List<String>> runSearch(String q) async {
  await Future<void>.delayed(const Duration(seconds: 2));
  return <String>['hit:$q'];
}
📊 Production Insight
A marketplace app debounced searches but never tagged requests: an old slow response overwrote newer results and then crashed on back navigation. Request ids fixed both symptoms at once.
🎯 Key Takeaway
After every await, assume disposal won the race — guard with mounted and ignore stale responses by request id.

Canceling Timers, Streams, and Controllers in Dispose

Mounted guards stop crashes but not leaks. A periodic Timer that fires setState every second will keep firing into your guard forever — no crash, but a dead State waking up pointlessly and a State object the garbage collector cannot free because the timer holds it. Multiply by every visit to the screen and you get climbing memory, wasted battery, and callbacks from three visits ago mutating shared services.

The rule is ownership: the State that creates a recurring source must kill it in dispose. Store Timers and StreamSubscriptions in fields, cancel them in dispose, and null the fields so double-dispose stays safe. Dispose AnimationControllers, TextEditingControllers, FocusNodes, and ScrollControllers you created — but never dispose controllers handed to you by a parent, since the owner disposes.

Order matters inside dispose. Cancel subscriptions and timers first so no callback can fire mid-teardown, dispose controllers next, and call super.dispose last. A callback that fires while half your fields are torn down produces weirder crashes than the original — so silence every source before dismantling anything. A dispose checklist taped to the team's review template beats rediscovering the order during every incident.

io/thecodeforge/flutter/cancel_in_dispose.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import 'dart:async';
import 'package:flutter/material.dart';

class TickerScreen extends StatefulWidget {
  const TickerScreen({super.key});

  @override
  State<TickerScreen> createState() => _TickerScreenState();
}

class _TickerScreenState extends State<TickerScreen> {
  Timer? _timer;
  StreamSubscription<int>? _sub;
  int _ticks = 0;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
      if (!mounted) return;
      setState(() => _ticks++);
    });
    _sub = tickStream().listen((_) {
      if (!mounted) return;
      setState(() => _ticks++);
    });
  }

  @override
  void dispose() {
    _timer?.cancel(); // silence recurring sources first
    _timer = null;
    _sub?.cancel();
    _sub = null;
    super.dispose(); // framework teardown goes last
  }

  @override
  Widget build(BuildContext context) => Scaffold(body: Center(child: Text('$_ticks')));
}

Stream<int> tickStream() async* {
  var i = 0;
  while (true) {
    await Future<void>.delayed(const Duration(seconds: 1));
    yield i++;
  }
}
⚠ Guards Stop Crashes, Cancellation Stops Leaks
A mounted check without dispose cancellation leaves timers and subscriptions firing into a dead State forever. Always do both: guard the callback and cancel the source.
📊 Production Insight
A fitness app guarded its step-counter timer with mounted but never canceled it — users returning to the tracker found ten ghost timers ticking and battery draining overnight.
🎯 Key Takeaway
Own what you create: cancel timers and subscriptions in dispose, dispose your controllers, and call super.dispose last.

FutureBuilder and StreamBuilder: Let the Framework Own It

Hand-rolled fetch-then-setState is where the bug breeds, and FutureBuilder removes the breeding ground. You hand the framework a Future and a builder, and it handles subscription, rebuilds, and disposal: when the widget leaves the tree, the builder goes with it and late results have nowhere to land. No mounted check needed because there is no manual setState at all.

Use them with discipline. Create the Future once in initState and store it in a field — building a fresh Future inside build refires the request on every rebuild and spins forever. Handle all three snapshot states explicitly: waiting shows a spinner, error shows a retry, data shows content. A builder that only handles data will flash blank screens and swallow the errors you need to see.

StreamBuilder extends the same safety to live data like Firestore snapshots and location updates: it subscribes on mount and unsubscribes on dispose automatically. When the async work belongs to business logic rather than one screen, move it into a state-management layer whose lifetime exceeds any single route — but keep FutureBuilder as the default for screen-scoped loads. When reviewers see manual fetch-then-setState, they should ask why a builder was not enough.

io/thecodeforge/flutter/profile_future_builder.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import 'package:flutter/material.dart';

class ProfilePage extends StatefulWidget {
  const ProfilePage({super.key, required this.userId});
  final String userId;

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  late final Future<String> _name;

  @override
  void initState() {
    super.initState();
    _name = fetchName(widget.userId); // created once, never in build
  }

  @override
  Widget build(BuildContext context) {
    // Framework owns subscription and disposal: no manual setState.
    return Scaffold(
      body: Center(
        child: FutureBuilder<String>(
          future: _name,
          builder: (context, snap) {
            if (snap.connectionState == ConnectionState.waiting) {
              return const CircularProgressIndicator();
            }
            if (snap.hasError) return Text('Failed: ${snap.error}');
            return Text('Hello, ${snap.data}!');
          },
        ),
      ),
    );
  }
}

Future<String> fetchName(String id) async {
  await Future<void>.delayed(const Duration(seconds: 2));
  return 'Ada $id';
}
📊 Production Insight
A news app replaced forty hand-rolled fetch handlers with FutureBuilder and closed its entire after-dispose crash category in one release — plus deleted 600 lines.
🎯 Key Takeaway
Create the Future once in initState, handle waiting and error states, and let the framework own subscription and disposal.

Listeners, Controllers, and addPostFrameCallback Traps

Not every callback is a Future. AnimationController listeners, TextEditingController listeners, and ScrollController callbacks all fire synchronously from the framework — usually safe, but dangerous inside disposal itself or after a route pop races a fling animation. The same guard habit applies: any listener that calls setState should confirm the State is still mounted, especially listeners on objects shared across screens.

WidgetsBinding addPostFrameCallback is a subtler trap. Developers schedule a setState after the current frame to read sizes or show dialogs — but if the user pops the route before that frame runs, the callback fires on a disposed State. Guard post-frame callbacks with mounted too, or better, do the work in a place with a real lifecycle like didChangeDependencies.

Shared controllers deserve extra care. A PageController or audio player owned by a parent but listened to by a child must have the child's listener removed in the child's dispose — removing in the owner's dispose is too late if the child dies first. Whenever listener and owner have different lifetimes, the shorter-lived side must unsubscribe itself. Document listener ownership in a comment above every addListener so the next reader knows exactly who unsubscribes.

io/thecodeforge/flutter/listener_safety.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import 'package:flutter/material.dart';

class SizeReader extends StatefulWidget {
  const SizeReader({super.key});

  @override
  State<SizeReader> createState() => _SizeReaderState();
}

class _SizeReaderState extends State<SizeReader> {
  final ScrollController _scroll = ScrollController();
  double _offset = 0;

  @override
  void initState() {
    super.initState();
    _scroll.addListener(_onScroll);
    // Post-frame work can outlive the route: guard it.
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return;
      setState(() => _offset = _scroll.hasClients ? _scroll.offset : 0);
    });
  }

  void _onScroll() {
    if (!mounted) return;
    setState(() => _offset = _scroll.offset);
  }

  @override
  void dispose() {
    _scroll.removeListener(_onScroll);
    _scroll.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: ListView(controller: _scroll, children: [Text('offset $_offset')]));
  }
}
📊 Production Insight
A video app's post-frame size reader crashed on fast back-swipes: the frame callback fired one frame after the route popped. One mounted line ended a month-long crash tail.
🎯 Key Takeaway
Guard listeners and post-frame callbacks with mounted, and unsubscribe the shorter-lived side in its own dispose.

Proving Disposal Safety With Tests and DevTools

Lifecycle bugs hide from manual QA because testers rarely navigate with exact bad timing. Widget tests reproduce it deterministically: pump the screen, start the async load, pop the route before the Future completes, complete the Future, pump again, and assert tester.takeException is null. Use a Completer or a controllable fake so timing is exact rather than sleep-based — flaky timing tests get disabled, and disabled tests protect nothing.

DevTools catches the leak half of the problem. Open the memory view, push and pop the suspect screen ten times, force garbage collection, and count surviving State instances. Survivors mean a timer, subscription, or listener still holds them. The widget inspector's timeline also shows rebuilds: a disposed screen that keeps rebuilding is burning CPU against pixels nobody sees.

Make both checks permanent. A dispose-safety test per async screen plus a review checklist item — mounted after every await, cancellation in every dispose — turns a crash category into a solved problem. Teams that adopt the checklist watch after-dispose crashes disappear from their top-ten lists within a release or two. Publish the leak counts in the release notes so the whole team sees the trend.

📊 Production Insight
A banking team added pop-mid-request tests to fourteen screens and caught three live leaks in the first run — all in code that had passed manual QA for months.
🎯 Key Takeaway
Test pop-mid-request with a controllable Future, assert takeException is null, and verify no State instances survive in DevTools memory view.
● Production incidentPOST-MORTEMseverity: high

Slow Search API Crashed 9% of Sessions After Back Navigation

Symptom
Two days after release 3.8, crash reporting showed a new top crash: setState() called after dispose(), hitting 9 percent of sessions and concentrated on the search flow. The stack pointed at the search screen's result handler. Affected users saw a red error screen flash after pressing back from a result they had just opened — the worst moment to break trust, right after the app appeared to work. The backend was healthy and no deploy had touched search code that week, which sent the team hunting in the wrong place for half a day.
Assumption
The team assumed the crash came from a recent search-ranking change because the stack mentioned the results list. They rolled back the ranking service and watched — crash rate did not move. They then assumed a Flutter upgrade had tightened lifecycle checks, since the upgrade had landed in the same release train. That theory died when they reproduced the crash on the old SDK too. The real trigger was simpler: a backend slowdown had pushed p95 search latency from 400 milliseconds to 2.1 seconds, widening the window in which users navigated away before responses arrived.
Root cause
Each keystroke started a debounced search Future whose continuation called setState unconditionally. With responses now taking over two seconds, users routinely tapped a result and pressed back before the in-flight request completed. The continuation then called setState on the disposed search State. A periodic trending-terms Timer compounded it by firing setState after disposal too. Slow backend plus unguarded continuations turned normal navigation into a crash.
Fix
The hotfix added if (!mounted) return before every setState after an await on the search screen and canceled the trending Timer in dispose, shipping as 3.8.1 within 30 hours and cutting the crash to zero. The follow-up replaced the hand-rolled debounce with a cancellable search controller owned by the screen's dispose, added a widget test that pumps the screen, pops it mid-request, then completes the Future and asserts no exception, and added a lifecycle lint review to the team's pull-request checklist.
Key lesson
  • Treat every await as a navigation point: any code after it must assume the widget may be gone, so guard with mounted or restructure so stale results cannot reach setState.
  • Backend latency is a client crash vector. A p95 jump from 400 milliseconds to 2 seconds turned a latent lifecycle bug into a 9-percent crash — track client crashes against backend latency on the same dashboard.
  • Cancel recurring sources in dispose even when you add mounted guards. Guards stop the crash; cancellation stops the leak, the battery drain, and the next engineer's confusion.
Production debug guideFive steps from crash stack to guarded, leak-free async code.5 entries
Symptom · 01
Crash reporting shows setState() called after dispose() with a stack into your screen
→
Fix
Open the stack's first frame in your code — it names the exact continuation. Reproduce locally with flutter run, then throttle the network (DevTools network throttling or a proxy delay) so responses land slowly. Navigate to the screen, trigger the load, pop the route before it completes, and watch the exception fire. Slow responses plus fast navigation is the reliable recipe.
Symptom · 02
You found the setState but are unsure which async source outlived the widget
→
Fix
Search the State file for await, Timer, StreamSubscription, addListener, and AnimationController. Any of these whose callback reaches setState is a suspect. Add a print in dispose logging the widget runtime type, then trigger the flow: if the callback's print appears after the dispose print, that source outlived the widget. Guard one-shot Futures with mounted and cancel recurring sources outright.
Symptom · 03
A Timer or Stream keeps firing after you leave the screen
→
Fix
Store the Timer or StreamSubscription in a field, then cancel it in dispose and null the field. Verify with DevTools memory view: leave and re-enter the screen ten times and confirm State instances do not accumulate. If they pile up, a subscription is still holding them — grep for listen calls without a matching cancel in dispose.
Symptom · 04
AnimationController or TextEditingController throws after disposal
→
Fix
Dispose every controller you create in dispose, in reverse creation order, calling super.dispose last. Run flutter analyze plus a widget test that pumps the screen, pops it, pumps a frame, and asserts no exception. Controllers throw distinct dispose errors, so matching the message to the missing dispose call is usually instant.
Symptom · 05
You want a test that proves the crash cannot return
→
Fix
Write a widget test with a controllable Future: pump the screen, start the load, pop the route, then complete the Future and pump again. Assert tester.takeException is null. Run it via flutter test test/search_dispose_test.dart and keep it in CI. This test fails on any unguarded continuation, guarding the fix forever.
setState After Dispose Causes at a Glance
Root CauseHow to ConfirmFixPrevention
Network Future completes after back navigationThrottle network, load, pop route, watch exception fireAdd if (!mounted) return before setStatePop-mid-request widget test on every async screen
Periodic Timer firing past disposalLeave screen, observe callbacks or battery use continuingCancel timer in dispose and null the fieldReview rule: every Timer creation needs a dispose cancel
Stream subscription outliving the screenState instances accumulate in DevTools memory viewCancel subscription in disposePrefer StreamBuilder, which unsubscribes automatically
Undisposed Animation or text controllersController-specific dispose error in crash logsDispose owned controllers, super.dispose lastflutter analyze plus pump-and-pop test per screen
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
iothecodeforgeflutterlifecycle_guard.dartclass ProfileScreen extends StatefulWidget {The Lifecycle Behind the Error
iothecodeforgeflutterstale_search_guard.dartclass SearchScreen extends StatefulWidget {The Async Gap
iothecodeforgefluttercancel_in_dispose.dartclass TickerScreen extends StatefulWidget {Canceling Timers, Streams, and Controllers in Dispose
iothecodeforgeflutterprofile_future_builder.dartclass ProfilePage extends StatefulWidget {FutureBuilder and StreamBuilder
iothecodeforgeflutterlistener_safety.dartclass SizeReader extends StatefulWidget {Listeners, Controllers, and addPostFrameCallback Traps

Key takeaways

1
Every await is a gap where disposal can happen
guard what follows with a mounted check.
2
Cancel timers, subscriptions, and owned controllers in dispose; call super.dispose last.
3
Prefer FutureBuilder and StreamBuilder so the framework owns subscription and disposal.
4
Tag overlapping requests with ids so stale responses cannot overwrite fresh state.
5
Guard listeners and post-frame callbacks too, and unsubscribe the shorter-lived side.
6
Lock it with pop-mid-request widget tests and DevTools memory checks in review.

Common mistakes to avoid

5 patterns
×

Calling setState after every await with no mounted check

Symptom
Intermittent setState() called after dispose() crashes that spike on slow networks, when users navigate away mid-request.
Fix
Add if (!mounted) return immediately after each await in State methods, before any setState or context use.
×

Guarding with mounted but never canceling timers or streams

Symptom
No crash, but State instances pile up in memory view and callbacks keep firing against dead screens, draining battery.
Fix
Cancel every Timer and StreamSubscription in dispose. Guards stop crashes; cancellation stops leaks.
×

Creating the Future inside FutureBuilder's build method

Symptom
The request refires on every rebuild, spinners flash forever, and overlapping continuations multiply dispose races.
Fix
Create the Future once in initState, store it in a late final field, and pass that field to FutureBuilder.
×

Disposing a controller owned by someone else

Symptom
Double-dispose exceptions when the real owner tears down, or use-after-dispose when sibling widgets still need the controller.
Fix
Dispose only what you created. Remove your own listeners in dispose, but leave shared controllers to their owner.
×

Calling super.dispose() first in dispose

Symptom
Callbacks firing mid-teardown hit half-dismantled framework state, producing confusing secondary crashes that mask the original.
Fix
Cancel sources and dispose controllers first, then call super.dispose() last as the final line.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does setState() called after dispose() mean?
Q02JUNIOR
What is mounted and when should you check it?
Q03SENIOR
Why isn't a mounted check alone enough for Timers and Streams?
Q04SENIOR
How does FutureBuilder avoid this whole bug class?
Q05SENIOR
How would you write a widget test that proves disposal safety?
Q01 of 05JUNIOR

What does setState() called after dispose() mean?

ANSWER
An async callback or listener called setState on a State whose widget already left the tree. Between the operation's start and its callback, dispose ran — usually from navigation — so there is no element left to rebuild.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I just wrap setState in try-catch instead?
02
Is checking mounted before every setState wasteful?
03
Does FutureBuilder need a mounted check?
04
What about setState in initState?
05
Who disposes a shared controller?
06
Why do these crashes spike on slow networks?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Notes here come from systems that actually shipped.

Follow
✓ Verified
production tested
September 26, 2026
last updated
2,085
articles · all by Naren
🔥

That's Flutter. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
RenderFlex Overflow: Fix the Yellow Stripe Fast
2 / 7 · Flutter
Next
Gradle Build Failed: Fix the Minimum Version
→