ZetCode

Dart Timer

last modified May 29, 2026

Introduction

The Timer class from dart:async lets you schedule code to run after a delay or repeatedly at fixed intervals. It is the foundation of all time-based operations in Dart: countdowns, polling loops, debounced inputs, and animation frames all rely on timers under the hood.

Dart provides two timer variants:

Both return a Timer object that can be cancelled at any time before the callback fires. Timers work with the event loop: they do not block the main thread but instead schedule their callbacks to run in a future microtask or event loop tick, keeping the application responsive.

How Timers interact with the event loop

Dart’s event loop runs continuously, picking up events from queues. When a timer fires, its callback is added to the event queue (not the microtask queue). This means the callback runs after all pending microtasks have completed and the current event loop tick finishes. Because timers use the event queue, they are safe for non-blocking work — but you should still avoid heavy computation inside timer callbacks to keep the UI smooth.

event_loop_order.dart
import 'dart:async';

void main() {
  print('Start');

  // Timer callback goes to event queue
  Timer(const Duration(seconds: 0), () => print('Timer callback'));

  // Microtask runs before the timer callback
  scheduleMicrotask(() => print('Microtask 1'));

  // Future immediate also schedules a microtask
  Future.microtask(() => print('Microtask 2'));

  print('End');
  // Output:
  // Start
  // End
  // Microtask 1
  // Microtask 2
  // Timer callback
}

Even a zero-duration timer does not run immediately. It waits until the current execution context yields and the event loop picks up the timer event. This ordering is important when you need to defer a computation without blocking the microtask queue.

One-shot Timer

Create a one-shot timer with Timer(Duration delay, void Function() callback). The callback executes once after delay has elapsed.

simple_timer.dart
import 'dart:async';

void main() {
  print('Waiting for the timer…');

  final timer = Timer(const Duration(seconds: 2), () {
    print('2 seconds have passed!');
  });

  // The program stays alive until the timer fires
  // (or press Enter to quit early)
  stdin.listen((_) => timer.cancel());
}

The timer callback runs on the event loop; while the timer is pending, the program remains alive. If the callback is the last piece of work, the isolate may exit immediately after it finishes.

Periodic Timer (Timer.periodic)

Timer.periodic(Duration interval, void Function(Timer timer) callback) creates a timer that fires every interval until cancelled. The callback receives the timer itself, so you can cancel it from within.

periodic_timer.dart
import 'dart:async';

void main() {
  int tick = 0;

  final timer = Timer.periodic(const Duration(seconds: 1), (timer) {
    tick++;
    print('Tick $tick');
    if (tick >= 5) {
      timer.cancel(); // stop after 5 ticks
      print('Timer stopped.');
    }
  });

  print('Periodic timer started.');
}

The interval is measured from the start of one callback invocation to the start of the next. If a callback takes longer than the interval, the next invocation is skipped and will fire after the next full interval.

Cancelling a timer

Both Timer and Timer.periodic return a Timer object with a cancel() method. Calling cancel() prevents any future callbacks from executing. It is safe to call cancel() multiple times; after the first call, subsequent invocations have no effect.

cancel_timer.dart
import 'dart:async';

void main() {
  final timer = Timer(const Duration(seconds: 5), () {
    print('This will never print.');
  });

  // Cancel immediately – the callback is never called.
  timer.cancel();
  print('Timer cancelled before it fired.');
}

Cancelling a timer is the recommended way to clean up when the result of the delayed work is no longer needed, for example when a widget is removed from the screen or a search query is replaced.

Timer vs Future.delayed

Future.delayed also runs code after a delay, but it returns a Future that completes when the callback runs. A Timer is a lower-level construct; you cannot await it directly and it does not produce a value.

FeatureTimerFuture.delayed
ReturnsTimer object (for cancellation)Future<void> (or a typed value)
CancellationYes, via cancel()No built-in cancellation (but you can use a Completer)
AwaitableNoYes
PeriodicYes (Timer.periodic)No (must recreate)

Use Future.delayed when you need to delay an async operation and want to await the result. Use Timer when you need cancellation or a repeating schedule.

Countdown timer

A countdown uses a periodic timer that decrements a counter every second and stops when reaching zero.

countdown.dart
import 'dart:async';

void countdown(int seconds) {
  int remaining = seconds;

  final timer = Timer.periodic(const Duration(seconds: 1), (timer) {
    if (remaining > 0) {
      print('$remaining...');
      remaining--;
    } else {
      timer.cancel();
      print('Liftoff!');
    }
  });
}

void main() {
  countdown(5);
}

The periodic callback fires every second, printing the remaining seconds. After reaching zero, the timer cancels itself and prints a final message.

Periodic data polling

A common use case is polling a remote API for updates. The example below simulates a fetch every two seconds and stops after a set number of polls.

data_polling.dart
import 'dart:async';

// Simulate an async API call
Future<String> fetchStatus() async {
  await Future.delayed(const Duration(milliseconds: 200));
  return DateTime.now().millisecondsSinceEpoch.toString();
}

void main() {
  int pollCount = 0;
  const maxPolls = 5;

  final timer = Timer.periodic(const Duration(seconds: 2), (timer) async {
    final status = await fetchStatus();
    pollCount++;
    print('Poll $pollCount: $status');

    if (pollCount >= maxPolls) {
      timer.cancel();
      print('Polling stopped after $maxPolls requests.');
    }
  });
}

Even though the callback is marked async, the periodic timer does not wait for the future to complete before starting the next tick. If the fetch takes longer than the interval, multiple requests may overlap. For production use, consider adding a flag to avoid concurrent polls.

Debounce using Timer

In UI applications, you often want to delay an action until the user stops typing. A debouncer resets a timer on every keystroke.

debounce.dart
import 'dart:async';

class Debouncer {
  final Duration delay;
  Timer? _timer;

  Debouncer({required this.delay});

  void call(void Function() action) {
    _timer?.cancel();
    _timer = Timer(delay, action);
  }

  void dispose() => _timer?.cancel();
}

void main() {
  final debouncer = Debouncer(delay: const Duration(milliseconds: 300));

  // Simulate rapid keystrokes
  const keystrokes = ['H', 'e', 'l', 'l', 'o', '!'];
  for (final char in keystrokes) {
    debouncer.call(() => print('Search query: $char'));
  }

  // After 300ms of silence, the last action fires once.
  // Output (after delay):
  // Search query: !
}

The debouncer cancels any pending timer each time call is invoked, ensuring that the action runs only after a quiet period. This pattern is ubiquitous in search fields and auto-save features.

Multi-purpose timer utility

The program below combines a countdown, a periodic logger, and a cancellable delay into a single script that demonstrates the core timer concepts.

timer_demo.dart
import 'dart:async';
import 'dart:io';

void main() {
  stdout.writeln('=== Dart Timer Demo ===\n');

  // 1. One-shot timer
  stdout.writeln('Starting one-shot timer (3 seconds)...');
  final oneShot = Timer(const Duration(seconds: 3), () {
    stdout.writeln('One-shot timer fired!');
  });

  // 2. Periodic timer (countdown)
  int count = 5;
  stdout.writeln('Countdown:');
  final periodic = Timer.periodic(const Duration(seconds: 1), (timer) {
    if (count > 0) {
      stdout.writeln(count);
      count--;
    } else {
      stdout.writeln('Blast off!');
      timer.cancel();
    }
  });

  // 3. Cancellable delay: user can press Enter to abort
  final delayTimer = Timer(const Duration(seconds: 10), () {
    stdout.writeln('10-second delay elapsed (or was cancelled).');
  });

  stdout.writeln('Press Enter to cancel the 10-second timer...');
  stdin.listen((_) {
    delayTimer.cancel();
    stdout.writeln('Delay timer cancelled by user.');
    oneShot.cancel();  // also clean up the one-shot
    periodic.cancel(); // and periodic
    exit(0);
  });
}

The script starts a one-shot timer, a periodic countdown, and a long delay that the user can cancel by pressing Enter. It illustrates creation, cancellation, and the interaction of multiple timers running simultaneously.

Source

Timer class documentation, Dart event loop guide, Dart Futures tutorial

In this tutorial we covered the Timer class: one-shot and periodic timers, their relation to the event loop, cancellation, comparison with Future.delayed, and practical examples including a countdown, polling, and a debouncer.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.