FX04: State Update After Disposal
Prevent unhandled asynchronous exceptions by cancelling active tasks and guarding setState with mounted
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;`.
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)),
),
],
),
);
}
}