Skip to main content
Back to Fix Lab Hub
Verified: Flutter 3.24.3 • Dart 3.5.3
Async & LifecycleFX04Canonical Technical Guide

FX04: State Update After Disposal

Prevent unhandled asynchronous exceptions by cancelling active tasks and guarding setState with mounted

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.

Verified Correction Rule

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 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 Engine390pxlight

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)),
          ),
        ],
      ),
    );
  }
}