Skip to main content
Tested on: Flutter 3.24.3 • Dart 3.5.3
Async & LifecycleFX04Canonical Technical Guide

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.

Direct Answer & Quick Fix

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().

Realistic Production Scenario: Checkout Payment & User Profile Network Request
Application Context:

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.

Production Failure Impact:

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.

APIs:State.setStateState.disposeState.mountedTimer.cancel

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.

Root Cause Analysis

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.

Correction Rule

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 Protocol
1. Constraint Propagation

When a route is removed or replaced, Flutter's element reconciliation algorithm unmounts the State object, invoking deactivate() followed by dispose().

2. RenderObject Sizing

During dispose(), the framework detaches the State's element reference (_element = null) and updates its internal lifecycle enum to _StateLifecycle.defunct.

3. Why the Default Fails

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 Cancellation
❌ Broken Lifecycle (Orphaned Event Handle)
1. initState(): Starts Timer / async Future
2. Navigator.pop(): Triggers dispose() teardown
3. dispose(): Omits timer.cancel() (LEAK)
4. Delayed Tick: Invokes callback on defunct State
5. setState() throws FlutterError (CRASH)

State object has defunct lifecycle state (_element is null). Framework asserts immediately.

✅ Guarded Lifecycle (Cancelled & Guarded)
1. initState(): Stores _timer handle reference
2. Navigator.pop(): Triggers dispose() teardown
3. dispose(): Calls _timer?.cancel() (CLEAN)
4. In-Flight Guard: checks if (!mounted) return;
5. Zero crashes, zero leaked event handlers

Cancelling the timer frees the OS event loop handle. The mounted guard rejects any pending microtasks.

Evidence Mode: Live Flutter Engine
Width:
Correction Strategy:

_timer?.cancel() in dispose() + if (mounted) guard

Real Flutter Web CanvasKit Engine390px • • light

Corrected: Cancel in dispose() & Guard with mounted

Cancel the timer in dispose() and verify `if (!mounted) return;` before updating state.

Open in Studio
Key Architectural Modifications:
  • ✓Cancel active Timer/Subscription in dispose() override
  • ✓Check `if (!mounted) return;` prior to invoking setState()
standalone_fx04_fixed_390px.dartFlutter 3.24.3 • Zero External Dependencies • Runnable
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.

4 Strategies Audited
Explicit Resource Cancellation in dispose()Event Loop Handle Teardown (Best Practice)
@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.

State.mounted Early Return GuardAsync Continuation Protection
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.

BuildContext.mounted Guard (Flutter 3.7+)Context-Aware UI Actions
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.

State Management Controller ArchitectureDecoupled Business Logic (Bloc / Riverpod / GetX)
// 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

✓ All 4 Tests Passing
FLUTTER ENGINEFlutter 3.24.3 (stable)
DART SDKDart 3.5.3
ENGINE REVISION36335019a8
CLI TEST COMMANDflutter test test/fixes/fx04_lifecycle_test.dart

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.