Flutter setState() Called After Dispose — Fix & Example
The setState() called after dispose() error happens when an asynchronous operation, timer, or stream listener completes and attempts to update a State object that has already been unmounted. Learn how to verify the mounted property, cancel active subscriptions, and avoid memory leaks.
Why this happens: The 'setState() called after dispose()' error happens when a Future, timer, listener, stream, or async callback tries to update a State object after that widget has already been removed from the widget tree.
How to fix it: Always cancel active Timer and StreamSubscription instances in your State's dispose() method, and guard any asynchronous continuation with 'if (!mounted) return;' before calling setState().
A payment confirmation screen launches an asynchronous API charge (`await paymentService.processOrder()`). When the user taps "Submit", a network spinner appears. If the user encounters a slow network connection and taps the back button or closes the modal before the HTTP request returns, the route pops and the screen widget is unmounted.
When the HTTP response resolves 2 seconds later and invokes `setState(() => _isLoading = false)`, the framework crashes with a fatal assertion: `setState() called after dispose(): ... (lifecycle state: defunct, not mounted)`. In debug mode the app crashes; in release mode memory is leaked as defunct State objects remain referenced by the Dart event loop.
Observed Symptom & Flutter Error Assertion
setState() called after dispose() error when an asynchronous future completes.
════╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═════════════════════════════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
setState() called after dispose(): _MyWidgetState#82fa1(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree
(e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls
setState() from a timer or an animation callback.
The relevant error-causing widget was:
MyWidget
lib/main.dart:42:12
════════════════════════════════════════════════════════════════════════════════════════════════════Declared Conditions
Calling setState inside an uncancelled Timer, Stream subscription, or asynchronous Future.then() callback after the user has navigated away and the State object has unmounted.
Once a widget's State is removed from the widget tree, its lifecycle state is marked defunct and its element reference is cleared. Triggering setState() afterwards violates element lifecycle invariants and throws an assertion.
Always cancel owned Timers, stream subscriptions, and animation controllers inside dispose(), and guard any asynchronous continuation with `if (!mounted) return;`.
Architectural Deep Dive: Why This Failure Occurs in Flutter
Flutter Layout ProtocolWhen a route is removed or replaced, Flutter's element reconciliation algorithm unmounts the State object, invoking deactivate() followed by dispose().
During dispose(), the framework detaches the State's element reference (_element = null) and updates its internal lifecycle enum to _StateLifecycle.defunct.
Calling setState() asserts that `_lifecycleState != _StateLifecycle.defunct`. Dart timers and async futures operate on the underlying VM event loop independent of Flutter's widget tree. Without explicit cancellation or mounted checks, pending event callbacks execute on defunct State instances.
Flutter State Lifecycle & Ownership Architecture
R04 State.mounted & Event Loop CancellationState object has defunct lifecycle state (_element is null). Framework asserts immediately.
Cancelling the timer frees the OS event loop handle. The mounted guard rejects any pending microtasks.
_timer?.cancel() in dispose() + if (mounted) guard
Corrected: Cancel in dispose() & Guard with mounted
Cancel the timer in dispose() and verify `if (!mounted) return;` before updating state.
- ✓Cancel active Timer/Subscription in dispose() override
- ✓Check `if (!mounted) return;` prior to invoking setState()
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: Scaffold(body: Center(child: FixedTimerWidget()))));
class FixedTimerWidget extends StatefulWidget {
const FixedTimerWidget({super.key});
@override
State<FixedTimerWidget> createState() => _FixedTimerWidgetState();
}
class _FixedTimerWidgetState extends State<FixedTimerWidget> {
String _status = 'Countdown in progress (3s)...';
Timer? _timer;
@override
void initState() {
super.initState();
_timer = Timer(const Duration(seconds: 3), () {
// FIX 1: Guard against invocation after unmount
if (!mounted) return;
setState(() {
_status = 'Completed safely!';
});
});
}
@override
void dispose() {
// FIX 2: Cancel owned timer on teardown to release event loop handle
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF0FDF4),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF22C55E), width: 2),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle_rounded, color: Color(0xFF16A34A), size: 36),
const SizedBox(height: 12),
Text(
_status,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF14532D)),
),
const SizedBox(height: 6),
const Text(
'Timer cancelled in dispose() • if (mounted) guard active',
style: TextStyle(fontSize: 12, color: Color(0xFF15803D)),
),
],
),
);
}
}Alternative Fixes & Decision Framework
Compare Flutter architectural solutions to select the ideal strategy for your UX requirements.
@override
void dispose() {
_timer?.cancel();
_controller.dispose();
_subscription.cancel();
super.dispose();
}When to choose: Choose for any persistent or recurring event emitter that your widget creates (Timer, AnimationController, TextEditingController, StreamSubscription).
Trade-offs: Eliminates memory leaks and halts callbacks immediately. Must be paired with super.dispose() at the end.
Future<void> _submitData() async {
final result = await api.fetchData();
if (!mounted) return;
setState(() {
_data = result;
});
}When to choose: Choose after any `await` asynchronous suspension gap where the underlying Future cannot be directly aborted.
Trade-offs: Simple and reliable. Prevents calling setState on unmounted states without complex cancellation tokens.
Future<void> _saveAndNavigate() async {
await database.saveRecord();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Saved!')),
);
Navigator.pop(context);
}When to choose: Choose when showing dialogs, snackbars, or navigating after an asynchronous operation to satisfy the `use_build_context_synchronously` lint rule.
Trade-offs: Ensures the BuildContext element is still attached to the root render tree before accessing inherited widgets.
// Business logic runs outside widget lifecycle:
class OrderController extends ChangeNotifier {
Future<void> load() async {
final res = await api.fetch();
notifyListeners();
}
}
// UI listens via ListenableBuilder or Consumer:
ListenableBuilder(
listenable: controller,
builder: (context, _) => Text(controller.data),
)When to choose: Choose in production applications to decouple long-running network operations from ephemeral screen widget lifecycles entirely.
Trade-offs: Requires adopting a formal state management library, but completely removes widget lifecycle coupling from network calls.
Architectural Limitations & Edge Cases
- ⚠Simply checking `if (mounted) setState(...)` does NOT prevent memory leaks if periodic timers or stream subscriptions are left uncancelled in dispose().
- ⚠The `mounted` getter on `State` only checks if the widget's `_element` is currently attached; it does not protect against accessing invalid external state.
- ⚠In Flutter 3.7+, use `context.mounted` instead of `mounted` when checking context validity across async gaps in StatelessWidget or utility methods.
Common Mistakes to Avoid in Flutter
- •Calling setState() inside an uncancelled Future.then() or async gap without checking if (!mounted) return;
- •Forgetting to cancel StreamSubscription, Timer, or AnimationController instances in dispose()
- •Accessing BuildContext across asynchronous delays without checking if (context.mounted)
Automated Verification Proof & Toolchain Evidence
Verification Contract: Automated test suite tests unmounted State invoking setState after pop, catches defunct assertion in isolated zone, and verifies timer cancellation and if (!mounted) guards update state cleanly without exceptions.