Flutter
setState error
lifecycle methods
error handling
mobile development

Exception has occurred. FlutterError setState called after dispose _MyProfileStatec3ad1lifecycle state defunct, not mounted

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

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:

  1. initState() — Widget is inserted into the tree
  2. build() — Widget renders its UI
  3. deactivate() — Widget is temporarily removed
  4. 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.

dart
1// This causes the error:
2class MyProfilePage extends StatefulWidget {
3  
4  _MyProfileState createState() => _MyProfileState();
5}
6
7class _MyProfileState extends State<MyProfilePage> {
8  String? userName;
9
10  
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  
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():

dart
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:

dart
1class _MyProfileState extends State<MyProfilePage> {
2  Timer? _refreshTimer;
3  StreamSubscription? _dataSubscription;
4
5  
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  
21  void dispose() {
22    _refreshTimer?.cancel();         // Cancel the timer
23    _dataSubscription?.cancel();     // Cancel the stream subscription
24    super.dispose();
25  }
26
27  
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:

dart
1import 'package:async/async.dart';
2
3class _MyProfileState extends State<MyProfilePage> {
4  CancelableOperation<UserProfile>? _fetchOperation;
5
6  
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  
20  void dispose() {
21    _fetchOperation?.cancel();  // Cancel the pending request
22    super.dispose();
23  }
24
25  
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:

dart
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  
14  void dispose() {
15    _disposed = true;
16    super.dispose();
17  }
18}
19
20class MyProfilePage extends StatefulWidget {
21  
22  _MyProfileState createState() => _MyProfileState();
23}
24
25class _MyProfileState extends State<MyProfilePage> {
26  final _controller = ProfileController();
27
28  
29  void initState() {
30    super.initState();
31    _controller.fetchProfile();
32  }
33
34  
35  void dispose() {
36    _controller.dispose();
37    super.dispose();
38  }
39
40  
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:

dart
1class MyProfilePage extends StatelessWidget {
2  
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

Course illustration
Course illustration

All Rights Reserved.