Introduction
The error setState() called after dispose() occurs when you call setState() on a widget that has already been removed from the widget tree. This typically happens when an asynchronous operation (API call, timer, stream listener) completes after the user has navigated away from the screen. The widget's State object has been disposed, but the callback still tries to update it.
Why This Error Occurs
The Flutter widget lifecycle has four key phases:
initState() — Widget is inserted into the tree
build() — Widget renders its UI
deactivate() — Widget is temporarily removed
dispose() — Widget is permanently removed, resources should be cleaned up
After dispose() is called, the mounted property becomes false. Calling setState() at this point throws the error because there is no widget to rebuild.
1// This causes the error:
2class MyProfilePage extends StatefulWidget {
3 @override
4 _MyProfileState createState() => _MyProfileState();
5}
6
7class _MyProfileState extends State<MyProfilePage> {
8 String? userName;
9
10 @override
11 void initState() {
12 super.initState();
13 fetchUserProfile(); // Async call
14 }
15
16 Future<void> fetchUserProfile() async {
17 final response = await api.getUserProfile(); // Takes 3 seconds
18 // If user navigated away during these 3 seconds,
19 // this widget is disposed and setState() will crash:
20 setState(() {
21 userName = response.name; // ERROR: setState called after dispose
22 });
23 }
24
25 @override
26 Widget build(BuildContext context) {
27 return Text(userName ?? 'Loading...');
28 }
29}
Fix 1: Check mounted Before setState
The simplest fix is to check the mounted property before calling setState():
1Future<void> fetchUserProfile() async {
2 final response = await api.getUserProfile();
3
4 if (!mounted) return; // Widget was disposed — skip the update
5
6 setState(() {
7 userName = response.name;
8 });
9}
This is the recommended approach for most cases. The mounted getter returns true only if the State object is currently in the widget tree.
Fix 2: Cancel Async Operations in dispose()
For timers and stream subscriptions, cancel them in dispose() to prevent callbacks from firing:
1class _MyProfileState extends State<MyProfilePage> {
2 Timer? _refreshTimer;
3 StreamSubscription? _dataSubscription;
4
5 @override
6 void initState() {
7 super.initState();
8
9 _refreshTimer = Timer.periodic(Duration(seconds: 30), (_) {
10 fetchUserProfile();
11 });
12
13 _dataSubscription = dataStream.listen((data) {
14 if (mounted) {
15 setState(() => userName = data.name);
16 }
17 });
18 }
19
20 @override
21 void dispose() {
22 _refreshTimer?.cancel(); // Cancel the timer
23 _dataSubscription?.cancel(); // Cancel the stream subscription
24 super.dispose();
25 }
26
27 @override
28 Widget build(BuildContext context) {
29 return Text(userName ?? 'Loading...');
30 }
31}
Fix 3: Use a CancelableOperation
For HTTP requests, use CancelableOperation from the async package:
1import 'package:async/async.dart';
2
3class _MyProfileState extends State<MyProfilePage> {
4 CancelableOperation<UserProfile>? _fetchOperation;
5
6 @override
7 void initState() {
8 super.initState();
9 _fetchOperation = CancelableOperation.fromFuture(
10 api.getUserProfile(),
11 );
12 _fetchOperation!.value.then((profile) {
13 if (mounted) {
14 setState(() => userName = profile.name);
15 }
16 });
17 }
18
19 @override
20 void dispose() {
21 _fetchOperation?.cancel(); // Cancel the pending request
22 super.dispose();
23 }
24
25 @override
26 Widget build(BuildContext context) {
27 return Text(userName ?? 'Loading...');
28 }
29}
Fix 4: Use ValueNotifier or ChangeNotifier
Move state management out of the widget to avoid the lifecycle problem entirely:
1class ProfileController extends ChangeNotifier {
2 String? userName;
3 bool _disposed = false;
4
5 Future<void> fetchProfile() async {
6 final response = await api.getUserProfile();
7 if (!_disposed) {
8 userName = response.name;
9 notifyListeners();
10 }
11 }
12
13 @override
14 void dispose() {
15 _disposed = true;
16 super.dispose();
17 }
18}
19
20class MyProfilePage extends StatefulWidget {
21 @override
22 _MyProfileState createState() => _MyProfileState();
23}
24
25class _MyProfileState extends State<MyProfilePage> {
26 final _controller = ProfileController();
27
28 @override
29 void initState() {
30 super.initState();
31 _controller.fetchProfile();
32 }
33
34 @override
35 void dispose() {
36 _controller.dispose();
37 super.dispose();
38 }
39
40 @override
41 Widget build(BuildContext context) {
42 return ListenableBuilder(
43 listenable: _controller,
44 builder: (context, _) {
45 return Text(_controller.userName ?? 'Loading...');
46 },
47 );
48 }
49}
Fix 5: Use FutureBuilder
FutureBuilder handles the lifecycle automatically:
1class MyProfilePage extends StatelessWidget {
2 @override
3 Widget build(BuildContext context) {
4 return FutureBuilder<UserProfile>(
5 future: api.getUserProfile(),
6 builder: (context, snapshot) {
7 if (snapshot.connectionState == ConnectionState.waiting) {
8 return CircularProgressIndicator();
9 }
10 if (snapshot.hasError) {
11 return Text('Error: ${snapshot.error}');
12 }
13 return Text(snapshot.data?.name ?? 'Unknown');
14 },
15 );
16 }
17}
Common Pitfalls
Forgetting to cancel subscriptions: Every StreamSubscription, Timer, and AnimationController must be cancelled/disposed in dispose(). Failing to do so causes memory leaks and setState-after-dispose errors.
Checking mounted too early: Place the if (!mounted) return check immediately before setState(), not before the async call. The widget might get disposed between the await and the setState.
AnimationController: Always pass vsync: this and call controller.dispose() in dispose(). Forgetting to dispose an AnimationController is one of the most common sources of this error.
Global state: If using Provider, Riverpod, or Bloc, the state lives outside the widget lifecycle and this error does not occur. Consider using state management solutions for complex async flows.
Multiple awaits: If your method has several await calls, check mounted after each one, since the widget could be disposed between any two awaits.
Summary
setState() called after dispose() means an async callback tried to update a widget that no longer exists
Check if (!mounted) return before every setState() call in async methods
Cancel Timer, StreamSubscription, and AnimationController in dispose()
Use FutureBuilder or StreamBuilder to avoid manual lifecycle management
Consider state management solutions (Provider, Riverpod, Bloc) for complex async workflows