<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Event Loop - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Event Loop - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 23 Oct 2026 12:36:21 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/event-loop/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Advanced Dart: Learn Asynchronous Programming with Streams, Isolates, and the Event Loop ]]>
                </title>
                <description>
                    <![CDATA[ I had been writing Flutter apps for over a year before I actually understood how Dart handles concurrency. I knew how to use await. I knew FutureBuilder and StreamBuilder well enough to get things wor ]]>
                </description>
                <link>https://www.freecodecamp.org/news/advanced-dart-learn-async-programming-with-streams-isolates-event-loop/</link>
                <guid isPermaLink="false">6a3daf77210c3204fe177441</guid>
                
                    <category>
                        <![CDATA[ dart-isolates ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Event Loop ]]>
                    </category>
                
                    <category>
                        <![CDATA[ synchronous ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ single-threaded ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Gidudu Nicholas ]]>
                </dc:creator>
                <pubDate>Thu, 25 Jun 2026 22:45:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/bc97ef43-0f34-4cf1-a824-814a0ec2834d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>I had been writing Flutter apps for over a year before I actually understood how Dart handles concurrency.</p>
<p>I knew how to use <code>await</code>. I knew <code>FutureBuilder</code> and <code>StreamBuilder</code> well enough to get things working. But I didn't really understand what was happening underneath: why some code ran in a specific order, why certain operations froze my UI, or why stream subscriptions kept causing memory leaks I couldn't track down.</p>
<p>The moment I actually sat down and learned the event loop, everything else clicked. Why <code>mounted</code> checks work. Why <code>compute()</code> exists. Why streams behave differently depending on how many listeners you attach. These weren't separate things to memorize. They were all consequences of the same underlying model.</p>
<p>This article is the explanation I wish I'd had earlier. We'll go deep on how Dart's event loop actually works, how streams give you control over data that arrives over time, and how isolates let you escape the single thread when you need real parallelism — with practical Flutter examples throughout.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-how-darts-single-threaded-model-works">How Dart's Single-Threaded Model Works</a></p>
</li>
<li><p><a href="#heading-the-event-loop-and-its-two-queues">The Event Loop and Its Two Queues</a></p>
</li>
<li><p><a href="#heading-how-asyncawait-fits-into-this">How async/await Fits Into This</a></p>
</li>
<li><p><a href="#heading-streams-controlling-data-that-arrives-over-time">Streams: Controlling Data That Arrives Over Time</a></p>
</li>
<li><p><a href="#heading-streamtransformers-and-advanced-stream-control">StreamTransformers and Advanced Stream Control</a></p>
</li>
<li><p><a href="#heading-isolates-escaping-the-single-thread">Isolates: Escaping the Single Thread</a></p>
</li>
<li><p><a href="#heading-putting-it-all-together-in-flutter">Putting It All Together in Flutter</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-how-darts-single-threaded-model-works">How Dart's Single-Threaded Model Works</h2>
<p>Most languages let you run code on multiple threads simultaneously. One thread handles the network call, another handles user input, another renders the UI — all running at the same time in parallel.</p>
<p>Dart doesn't work that way. Dart runs everything on a single thread. One thing at a time. Always.</p>
<p>When I first learned this, it felt like a limitation. How could a single thread handle a network call, a user tapping a button, and rendering 60 frames per second simultaneously? The answer is that it doesn't handle them simultaneously — it handles them in turns, managed by the event loop.</p>
<p>Think of it like a chef working alone in a kitchen. One chef, one pair of hands. They can't chop and stir at the same time. But a good chef doesn't stand idle waiting for water to boil — they go prep vegetables, come back when the water's ready, then move to the next task. They stay productive by switching between tasks as each one becomes available.</p>
<p>Dart is that chef. The event loop is the system that decides which task to pick up next.</p>
<h2 id="heading-the-event-loop-and-its-two-queues">The Event Loop and Its Two Queues</h2>
<p>The event loop runs for the entire lifetime of your Dart app. Its job is simple: check if there's work to do, do it, then check again. It does this continuously, in a loop, until the app exits.</p>
<p>Work doesn't happen immediately in Dart. When something is ready to run — a network response arriving, a timer firing, a <code>.then()</code> callback completing — it gets added to a queue. The event loop processes items from those queues one at a time.</p>
<p>Dart has exactly two queues, and understanding both is what separates developers who use async from developers who truly understand it.</p>
<h3 id="heading-the-microtask-queue">The Microtask Queue</h3>
<p>This is the high-priority queue. The event loop always empties this queue completely before looking at anything else. <code>.then()</code> callbacks and <code>Future.microtask()</code> land here.</p>
<p>Think of it as the fast checkout lane: short, urgent tasks that should run as soon as possible after the current synchronous code finishes.</p>
<h3 id="heading-the-event-queue">The Event Queue</h3>
<p>This is where everything external goes — timer callbacks, network responses, user input events, stream data, and <code>Future.delayed()</code> completions. The event loop processes one item from this queue, then goes back to check the microtask queue before processing the next event.</p>
<p>Here's what that ordering looks like in practice:</p>
<pre><code class="language-dart">void main() {
  print('1 — synchronous, runs immediately');

  // Goes into the EVENT queue — regular lane
  Future.delayed(Duration.zero, () {
    print('4 — event queue');
  });

  // Goes into the MICROTASK queue — high priority lane
  Future.microtask(() {
    print('3 — microtask queue');
  });

  print('2 — synchronous, runs immediately');
}

// Output:
// 1 — synchronous, runs immediately
// 2 — synchronous, runs immediately
// 3 — microtask queue
// 4 — event queue
</code></pre>
<p>Items <code>1</code> and <code>2</code> run first because they're synchronous — no queue involved, just straight execution. Then <code>3</code> runs before <code>4</code> even though both were scheduled with zero delay, because microtasks always run before events.</p>
<p>This ordering matters more than it might seem. When you chain multiple <code>.then()</code> calls, each callback goes into the microtask queue — which is why they feel immediate and always run before any timer or I/O callback, even one scheduled with zero delay.</p>
<pre><code class="language-dart">void main() {
  Future(() =&gt; print('event 1'));
  Future(() =&gt; print('event 2'));
  Future.microtask(() =&gt; print('microtask 1'));
  Future.microtask(() =&gt; print('microtask 2'));
  print('synchronous');
}

// Output:
// synchronous
// microtask 1
// microtask 2
// event 1
// event 2
</code></pre>
<p>Both microtasks run before either event, regardless of the order they were scheduled in.</p>
<h2 id="heading-how-asyncawait-fits-into-this">How async/await Fits Into This</h2>
<p><code>async/await</code> doesn't create new threads. It doesn't run things in parallel. It's syntactic sugar built on top of the event loop, a cleaner way to write code that works with Dart's single-threaded concurrency model.</p>
<p>Here's the best way I've found to think about it. Imagine you're a waiter in a restaurant, and you're the only waiter on shift. You can only do one thing at a time, but you don't have to stand at the kitchen pass waiting for food. You hand the order to the kitchen and walk away. You go refill water, take another order, clear a table. When the kitchen rings the bell, you pick up the food and deliver it.</p>
<p><code>await</code> is that moment of handing the order to the kitchen and walking away. You're not blocking, you're pausing this particular task and telling the event loop "come back to me when this is ready." The event loop can now handle other things while the network call, file read, or timer is in progress.</p>
<p>When the awaited operation completes, the rest of your function gets added to the queue and runs when the event loop gets back to it.</p>
<pre><code class="language-dart">Future&lt;void&gt; loadUser() async {
  print('A — before await');

  // Dart pauses here and hands control back to the event loop.
  // The event loop is now free to handle other work —
  // rendering frames, processing other futures, handling taps —
  // while the network call is in progress.
  final user = await dio.get('/user');

  // This only runs when the network response arrives
  // and the event loop gets back to this function.
  print('B — after await, got user');
}

void main() {
  loadUser();

  // This runs before B because loadUser() paused at the await
  // and returned control here before the network call completed.
  print('C — main continues');
}

// Output:
// A — before await
// C — main continues
// B — after await, got user
</code></pre>
<h3 id="heading-why-blocking-the-event-loop-causes-jank-in-flutter">Why Blocking the Event Loop Causes Jank in Flutter</h3>
<p>Flutter's UI rendering runs on the same main isolate as your Dart code. The engine needs the event loop to be free roughly every 16 milliseconds to render a frame at 60fps. Any synchronous operation that takes longer than that blocks the event loop completely — no frames get rendered, no taps get processed, the UI freezes.</p>
<pre><code class="language-dart">// This is dangerous in Flutter.
// Parsing a large JSON response synchronously
// can take 100-300ms on slower devices.
// The event loop is completely blocked the entire time.
// Flutter drops every frame during that window.
// The user sees a frozen screen.
final users = (response.data as List)
    .map((json) =&gt; User.fromJson(json))
    .toList();
</code></pre>
<p><code>await</code> doesn't help here because the work is CPU-bound — the CPU is busy the entire time, so there's no natural pause where the event loop can breathe. That's exactly the problem isolates exist to solve, which we'll get to shortly.</p>
<h2 id="heading-streams-controlling-data-that-arrives-over-time">Streams: Controlling Data That Arrives Over Time</h2>
<p>A <code>Future</code> delivers one value and completes. A <code>Stream</code> delivers multiple values over time and stays open until it's cancelled or exhausted.</p>
<p>If a <code>Future</code> is ordering food at a restaurant — you wait once, you get one meal, it's done — then a <code>Stream</code> is a subscription newsletter. New editions keep arriving over time, and you keep receiving them until you unsubscribe.</p>
<pre><code class="language-dart">// A stream that counts from 1 to 5, one number per second.
// async* marks this as a stream generator function.
// yield pushes a value into the stream and pauses
// until the listener is ready for the next value.
Stream&lt;int&gt; countStream() async* {
  for (int i = 1; i &lt;= 5; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
  // When the loop ends the stream closes automatically.
}
</code></pre>
<p>You can consume a stream with <code>await for</code> or with <code>.listen()</code>:</p>
<pre><code class="language-dart">// Method 1 — await for: clean, readable for simple cases
await for (final number in countStream()) {
  print(number); // prints 1, 2, 3, 4, 5, one per second
}

// Method 2 — listen(): more control, can cancel midway
final subscription = countStream().listen(
  (number) =&gt; print(number),
  onError: (error) =&gt; print('Error: $error'),
  onDone: () =&gt; print('Stream closed'),
);

// Cancel after 3 seconds — stops receiving values
await Future.delayed(const Duration(seconds: 3));
subscription.cancel();
</code></pre>
<h3 id="heading-single-subscription-vs-broadcast-streams">Single-Subscription vs Broadcast Streams</h3>
<p>This distinction trips up a lot of Flutter developers, and understanding it prevents a whole category of confusing errors.</p>
<p><strong>Single-subscription streams</strong> can only have one listener at a time. This is the default. Most streams — file reads, HTTP response bodies — are single-subscription. Try to listen twice and you get a <code>StateError</code>.</p>
<pre><code class="language-dart">final stream = countStream();

stream.listen(print); // fine
stream.listen(print); // throws: Stream has already been listened to
</code></pre>
<p><strong>Broadcast streams</strong> can have any number of simultaneous listeners. All of them receive the same values. This is what you want for app-wide events, user interactions, or anything multiple parts of your app need to react to.</p>
<pre><code class="language-dart">// StreamController.broadcast() creates a stream
// that any number of listeners can subscribe to.
final controller = StreamController&lt;String&gt;.broadcast();

controller.stream.listen((v) =&gt; print('Listener 1: $v'));
controller.stream.listen((v) =&gt; print('Listener 2: $v'));

// Both listeners receive this value
controller.sink.add('Hello');
// Listener 1: Hello
// Listener 2: Hello

// Always close the controller when you're done with it.
// An unclosed controller keeps resources alive indefinitely.
controller.close();
</code></pre>
<h3 id="heading-using-streamcontroller-to-create-streams-manually">Using StreamController to Create Streams Manually</h3>
<p><code>StreamController</code> gives you full manual control. You decide exactly when to push values, when to push errors, and when to close the stream. This is how you build reactive data sources from scratch.</p>
<pre><code class="language-dart">class LocationService {
  // Broadcast so multiple widgets can listen to
  // location updates simultaneously.
  final _controller = StreamController&lt;Position&gt;.broadcast();

  // Expose only the stream publicly.
  // The controller stays private so only this class
  // can push new values into it.
  Stream&lt;Position&gt; get locationStream =&gt; _controller.stream;

  void startTracking() {
    Timer.periodic(const Duration(seconds: 2), (_) {
      final position = Position(lat: 0.3476, lng: 32.5825);
      // sink.add() pushes a value into the stream.
      // All active listeners receive it immediately.
      _controller.sink.add(position);
    });
  }

  void dispose() {
    // Always close the controller when you're done.
    // An unclosed controller is a memory leak.
    _controller.close();
  }
}
</code></pre>
<h3 id="heading-using-streams-in-flutter-with-streambuilder">Using Streams in Flutter with StreamBuilder</h3>
<p><code>StreamBuilder</code> is the Flutter widget for consuming a stream directly in the UI. It rebuilds every time a new value arrives.</p>
<pre><code class="language-dart">StreamBuilder&lt;List&lt;Message&gt;&gt;(
  stream: firestore
      .collection('messages')
      .snapshots()
      .map((snapshot) =&gt; snapshot.docs
          .map((doc) =&gt; Message.fromJson(doc.data()))
          .toList()),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }

    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }

    if (!snapshot.hasData || snapshot.data!.isEmpty) {
      return const Text('No messages yet');
    }

    return ListView.builder(
      itemCount: snapshot.data!.length,
      itemBuilder: (context, index) {
        return MessageBubble(message: snapshot.data![index]);
      },
    );
  },
)
</code></pre>
<h3 id="heading-always-cancel-stream-subscriptions-in-dispose">Always Cancel Stream Subscriptions in <code>dispose</code></h3>
<p>This is one of the most common memory leaks in Flutter apps, and it comes directly from not understanding streams.</p>
<p>An active subscription keeps the stream's callback alive. If the widget it belonged to is gone but the subscription is still running, callbacks fire on a disposed widget, <code>setState</code> gets called after <code>dispose</code>, and objects that should have been freed stay in memory.</p>
<pre><code class="language-dart">class _ChatScreenState extends State&lt;ChatScreen&gt; {
  StreamSubscription&lt;Message&gt;? _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = messageStream.listen((message) {
      if (mounted) setState(() =&gt; messages.add(message));
    });
  }

  @override
  void dispose() {
    // cancel() unsubscribes from the stream.
    // Without this, the callback keeps firing
    // even after this screen is removed from the tree.
    _subscription?.cancel();
    super.dispose();
  }
}
</code></pre>
<h2 id="heading-streamtransformers-and-advanced-stream-control">StreamTransformers and Advanced Stream Control</h2>
<p>Once you understand streams, you quickly discover that raw streams rarely give you exactly what you want. You need to filter values, transform them, debounce rapid emissions, or combine multiple streams. That's where stream operators and <code>StreamTransformer</code> come in.</p>
<p>Dart's <code>Stream</code> class has a rich set of built-in transformation methods:</p>
<pre><code class="language-dart">final stream = countStream();

// map — transform each value before it reaches listeners
stream
    .map((number) =&gt; number * 2)
    .listen(print); // 2, 4, 6, 8, 10

// where — filter out values that don't match a condition
stream
    .where((number) =&gt; number.isEven)
    .listen(print); // 2, 4

// take — only emit the first N values, then close
stream
    .take(3)
    .listen(print); // 1, 2, 3

// skip — ignore the first N values
stream
    .skip(2)
    .listen(print); // 3, 4, 5

// distinct — only emit when the value changes from the last one
Stream.fromIterable([1, 1, 2, 2, 3])
    .distinct()
    .listen(print); // 1, 2, 3
</code></pre>
<p>For more complex transformations, you can build a custom <code>StreamTransformer</code>. This is the pattern to reach for when the built-in operators don't cover your use case — for example, when you need to transform values in a way that requires maintaining state between emissions.</p>
<pre><code class="language-dart">// A StreamTransformer that only emits values above a threshold
// and prefixes each one with a label.
StreamTransformer&lt;int, String&gt; aboveThreshold(int threshold) {
  return StreamTransformer.fromHandlers(
    handleData: (value, sink) {
      // sink.add() pushes a transformed value downstream.
      // If we don't call sink.add(), the value is filtered out.
      if (value &gt; threshold) {
        sink.add('Above threshold: $value');
      }
    },
    handleError: (error, stackTrace, sink) {
      // Forward errors downstream unchanged.
      sink.addError(error, stackTrace);
    },
    handleDone: (sink) {
      // Close the output stream when the input stream closes.
      sink.close();
    },
  );
}

// Usage
countStream()
    .transform(aboveThreshold(3))
    .listen(print);
// Above threshold: 4
// Above threshold: 5
</code></pre>
<h3 id="heading-debouncing-with-streams-in-flutter">Debouncing with Streams in Flutter</h3>
<p>One of the most practical stream patterns in Flutter apps is debouncing a search field. Without debouncing, every keystroke fires an API call. With debouncing, you wait for the user to stop typing before firing.</p>
<pre><code class="language-dart">class _SearchScreenState extends State&lt;SearchScreen&gt; {
  final _searchController = TextEditingController();
  final _searchStream = StreamController&lt;String&gt;();
  StreamSubscription? _subscription;
  List&lt;Result&gt; _results = [];

  @override
  void initState() {
    super.initState();

    _subscription = _searchStream.stream
        // Wait 300ms after the last keystroke before emitting.
        // If a new value arrives within 300ms, the timer resets.
        // This prevents firing an API call on every keystroke.
        .asyncExpand((query) async* {
          await Future.delayed(const Duration(milliseconds: 300));
          yield query;
        })
        // Ignore duplicate queries — no point re-fetching
        // if the user typed the same thing again.
        .distinct()
        // For each query, call the API and emit the results.
        // asyncMap cancels the previous call if a new query
        // arrives before the previous one completes.
        .asyncMap((query) =&gt; _repository.search(query))
        .listen((results) {
          if (mounted) setState(() =&gt; _results = results);
        });

    _searchController.addListener(() {
      _searchStream.add(_searchController.text);
    });
  }

  @override
  void dispose() {
    _searchController.dispose();
    _subscription?.cancel();
    _searchStream.close();
    super.dispose();
  }
}
</code></pre>
<h2 id="heading-isolates-escaping-the-single-thread">Isolates: Escaping the Single Thread</h2>
<p>Dart is single-threaded, but that doesn't mean you're limited to one thread forever. Isolates are Dart's way of running code on a completely separate thread — with one important difference from threads in other languages.</p>
<p>In most languages, threads share memory. Two threads can read and write the same variable at the same time, which creates race conditions and requires careful locking to prevent.</p>
<p>Dart isolates don't share memory at all. Each isolate has its own separate memory heap. The only way two isolates can communicate is by passing messages — like sending notes through a slot in a wall rather than sharing a whiteboard.</p>
<p>This makes isolates safe by design. There are no race conditions because there's nothing to race over. Each isolate owns its data completely.</p>
<pre><code class="language-plaintext">Main Isolate                    Worker Isolate
─────────────────               ─────────────────
Own memory heap                 Own memory heap
Own event loop                  Own event loop
UI rendering                    Heavy computation
User input                      No UI access
│                               │
│──── sends data ──────────────→│
│                               │ (processes independently)
│←─── receives result ──────────│
</code></pre>
<h3 id="heading-when-you-actually-need-an-isolate">When You Actually Need an Isolate</h3>
<p>The distinction that matters is CPU-bound vs I/O-bound work:</p>
<ul>
<li><p><strong>I/O-bound work</strong>: waiting for a network response, reading a file — just use <code>await</code>. The CPU is idle while waiting, so the event loop stays free.</p>
</li>
<li><p><strong>CPU-bound work</strong>: actually computing something, processing data, parsing large files — needs an isolate. The CPU is busy the whole time, so <code>await</code> can't help.</p>
</li>
</ul>
<p>If parsing your API response takes 200ms, <code>await</code> doesn't save you. The event loop is blocked for those 200ms regardless. You need to move that work to a separate isolate.</p>
<h3 id="heading-isolaterun-the-modern-approach"><code>Isolate.run()</code> — the Modern Approach</h3>
<p><code>Isolate.run()</code> was added in Dart 2.19 and is the cleanest way to run a one-off task in a background isolate. It spawns the isolate, runs your function, returns the result, and closes the isolate automatically.</p>
<pre><code class="language-dart">// In your repository:
Future&lt;List&lt;User&gt;&gt; getUsers() async {
  // Step 1 — network call is I/O-bound.
  // We await it and the event loop stays free while waiting.
  final response = await dio.get('/users');

  // Step 2 — parsing thousands of users is CPU-bound.
  // We move it to a separate isolate with Isolate.run().
  // The main isolate's event loop stays free the whole time.
  // Flutter keeps rendering frames normally.
  final users = await Isolate.run(() {
    final data = response.data as List&lt;dynamic&gt;;
    return data
        .map((json) =&gt; User.fromJson(json as Map&lt;String, dynamic&gt;))
        .toList();
  });

  return users;
}
</code></pre>
<h3 id="heading-compute-flutters-built-in-helper"><code>compute()</code> — Flutter's Built-in Helper</h3>
<p><code>compute()</code> is Flutter's wrapper around isolates that predates <code>Isolate.run()</code>. It's still widely used and works well, but has one constraint: the function you pass must be a top-level or static function, not a closure that captures local variables.</p>
<pre><code class="language-dart">// The function must be top-level or static.
// It can't be a closure because closures that capture
// state can't be sent across isolate boundaries.
List&lt;User&gt; parseUsers(dynamic data) {
  return (data as List)
      .map((json) =&gt; User.fromJson(json as Map&lt;String, dynamic&gt;))
      .toList();
}

// In your repository:
final users = await compute(parseUsers, response.data);
</code></pre>
<p>For most use cases, <code>Isolate.run()</code> is simpler and more flexible. <code>compute()</code> is still useful if you need to support Flutter versions below 2.19.</p>
<h3 id="heading-full-isolate-communication-with-sendport-and-receiveport">Full Isolate Communication with <code>SendPort</code> and <code>ReceivePort</code></h3>
<p>For long-running background tasks where you need to send multiple messages back and forth — a background sync service, a real-time data processor, a file watcher — you need a full isolate with <code>SendPort</code> and <code>ReceivePort</code>.</p>
<pre><code class="language-dart">void main() async {
  // ReceivePort is how the main isolate listens
  // for messages coming back from the worker.
  final receivePort = ReceivePort();

  // Spawn the worker isolate and give it a SendPort
  // so it can send messages back to us.
  await Isolate.spawn(
    workerFunction,
    receivePort.sendPort,
  );

  // Listen for messages from the worker.
  receivePort.listen((message) {
    print('Main received: $message');
  });
}

// This function runs entirely in the worker isolate.
// It has its own memory heap, completely separate
// from the main isolate. It cannot access any
// variables from main() directly.
void workerFunction(SendPort sendPort) {
  for (int i = 0; i &lt; 5; i++) {
    // sendPort.send() passes a message to the main isolate.
    // The message is copied, not shared — no shared memory.
    sendPort.send('Processed item $i');
  }
}
</code></pre>
<h3 id="heading-choosing-the-right-approach">Choosing the Right Approach</h3>
<table>
<thead>
<tr>
<th>Situation</th>
<th>Use</th>
</tr>
</thead>
<tbody><tr>
<td>One-off background task</td>
<td><code>Isolate.run()</code></td>
</tr>
<tr>
<td>Need to support Flutter below 2.19</td>
<td><code>compute()</code></td>
</tr>
<tr>
<td>Long-running background worker</td>
<td>Full isolate with <code>SendPort</code></td>
</tr>
<tr>
<td>Waiting for network or file I/O</td>
<td>Just <code>await</code> — no isolate needed</td>
</tr>
</tbody></table>
<h2 id="heading-putting-it-all-together-in-flutter">Putting It All Together in Flutter</h2>
<p>Here's a complete example that uses all three concepts together (the event loop, streams, and isolates) in a single Flutter feature: a search screen that fetches results from a mock API, parses them in a background isolate, and delivers them via a stream.</p>
<pre><code class="language-dart">import 'dart:isolate';
import 'package:flutter/material.dart';

// Model
class SearchResult {
  final String id;
  final String title;
  const SearchResult({required this.id, required this.title});
}

// Top-level function — required for Isolate.run()
// because it can't be a closure
List&lt;SearchResult&gt; parseResults(List&lt;dynamic&gt; data) {
  // Simulate expensive parsing work
  return data.map((item) =&gt; SearchResult(
    id: item['id'].toString(),
    title: item['title'] as String,
  )).toList();
}

// Repository
class SearchRepository {
  // Mock data — in a real app this would be a network call
  final List&lt;Map&lt;String, dynamic&gt;&gt; _mockData = List.generate(
    100,
    (i) =&gt; {'id': i, 'title': 'Result ${i + 1}'},
  );

  Future&lt;List&lt;SearchResult&gt;&gt; search(String query) async {
    // Simulate network delay
    await Future.delayed(const Duration(milliseconds: 500));

    // Filter mock data
    final filtered = _mockData
        .where((item) =&gt;
            (item['title'] as String)
                .toLowerCase()
                .contains(query.toLowerCase()))
        .toList();

    // Parse in a background isolate so the main
    // isolate's event loop stays free
    return Isolate.run(() =&gt; parseResults(filtered));
  }
}

// Screen
class SearchScreen extends StatefulWidget {
  const SearchScreen({super.key});

  @override
  State&lt;SearchScreen&gt; createState() =&gt; _SearchScreenState();
}

class _SearchScreenState extends State&lt;SearchScreen&gt; {
  final _controller = TextEditingController();
  final _repository = SearchRepository();

  bool _isLoading = false;
  List&lt;SearchResult&gt; _results = [];
  String? _error;

  Future&lt;void&gt; _search(String query) async {
    if (query.trim().isEmpty) {
      setState(() =&gt; _results = []);
      return;
    }

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final results = await _repository.search(query);

      // mounted check — the user might have navigated away
      // while the search was running
      if (!mounted) return;

      setState(() {
        _results = results;
        _isLoading = false;
      });
    } catch (e) {
      if (!mounted) return;

      setState(() {
        _error = 'Search failed. Please try again.';
        _isLoading = false;
      });
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Search')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: TextField(
              controller: _controller,
              decoration: const InputDecoration(
                labelText: 'Search',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.search),
              ),
              onChanged: _search,
            ),
          ),
          Expanded(child: _buildBody()),
        ],
      ),
    );
  }

  Widget _buildBody() {
    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (_error != null) {
      return Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(_error!),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () =&gt; _search(_controller.text),
              child: const Text('Try again'),
            ),
          ],
        ),
      );
    }

    if (_results.isEmpty) {
      return const Center(child: Text('No results found.'));
    }

    return ListView.builder(
      itemCount: _results.length,
      itemBuilder: (context, index) {
        final result = _results[index];
        return ListTile(
          leading: Text(result.id),
          title: Text(result.title),
        );
      },
    );
  }
}

void main() {
  runApp(const MaterialApp(home: SearchScreen()));
}
</code></pre>
<p>This example brings together everything we've covered:</p>
<ul>
<li><p>The <strong>event loop</strong> keeps the UI responsive while the mock network delay is in progress — <code>await</code> hands control back to the event loop so Flutter keeps rendering frames</p>
</li>
<li><p><strong>Isolates</strong> handle the parsing work in the background so even with a large result set the main thread stays free</p>
</li>
<li><p>The <strong>mounted check</strong> protects against the widget being disposed while the search is in flight</p>
</li>
<li><p>All four UI states (loading, error, empty, and results) are handled explicitly</p>
</li>
</ul>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Understanding the event loop, streams, and isolates helps you understand why Dart behaves the way it does. Once that mental model is in place, a lot of things that used to feel arbitrary start making sense.</p>
<p>Why do you need the <code>mounted</code> check? Because <code>await</code> pauses your function and returns control to the event loop — the widget can be disposed before your function resumes. Why does <code>compute()</code> help with jank? Because CPU-bound work blocks the event loop, and moving it to an isolate frees the loop to keep rendering. Why do broadcast streams exist? Because the default single-subscription stream only allows one listener, and some data sources need to serve multiple parts of your app simultaneously.</p>
<p>These aren't separate rules to memorize. They're all consequences of the same single-threaded concurrency model, once you understand it from the ground up.</p>
<p>If you're already comfortable with <code>await</code> and <code>FutureBuilder</code>, pick one concept from this article and go deeper on it this week. Build the stream debounce example. Try <code>Isolate.run()</code> on a real parsing task in one of your apps. Watch what happens to your frame rate in Flutter DevTools before and after. The understanding sticks much faster when you see it working in your own code.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How the Node.js Event Loop Works ]]>
                </title>
                <description>
                    <![CDATA[ The Node.js event loop is a concept that may seem difficult to understand at first. But as with any seemingly complex subject, the best way to understand it is often through an analogy. In this article, you’ll learn how overworked managers, busy wait... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-the-nodejs-event-loop-works/</link>
                <guid isPermaLink="false">68b86cca911797ceb3fd37e0</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Event Loop ]]>
                    </category>
                
                    <category>
                        <![CDATA[ asynchronous ]]>
                    </category>
                
                    <category>
                        <![CDATA[ synchronous ]]>
                    </category>
                
                    <category>
                        <![CDATA[ concurrency ]]>
                    </category>
                
                    <category>
                        <![CDATA[ parallelism ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Amanda Ene Adoyi ]]>
                </dc:creator>
                <pubDate>Wed, 03 Sep 2025 16:28:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1756916907320/01074df6-0f8e-4a63-9a3e-07c8297fc22b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The Node.js event loop is a concept that may seem difficult to understand at first. But as with any seemingly complex subject, the best way to understand it is often through an analogy.</p>
<p>In this article, you’ll learn how overworked managers, busy waiters, and train stations can help bring home the fundamental concept of the event loop. If you’re working with Node, you’ll need to understand how the event loop works, as it lies at the root of some of the most powerful applications today.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-synchronous-and-asynchronous-code">What are Synchronous and Asynchronous Code?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-concurrency-and-parallelism-mean">What Concurrency and Parallelism Mean</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-concurrency-in-nodejs">Concurrency in Node.js</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-parallelism-in-nodejs">Parallelism in Node.js</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-the-event-loop">What is the Event Loop?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-phases-of-the-event-loop">The Phases of the Event Loop</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>In order to seamlessly follow along with this article, it would help if you are familiar with the following concepts:</p>
<ol>
<li><p><strong>A basic understanding of JavaScript:</strong> Node.js runs on JavaScript, so you’ll need to understand variables, functions, and control flow.</p>
</li>
<li><p><strong>Familiarity with Node.js basics:</strong> Running simple scripts with Node and requiring modules.</p>
</li>
<li><p><strong>Some exposure to asynchronous patterns:</strong> Knowing what patterns such as <code>setTimeout()</code> do.</p>
</li>
<li><p><strong>Some familiarity with basic CPU concepts (cores and threads):</strong> This will help you better understand concurrency and parallelism.</p>
</li>
<li><p><strong>Awareness of promises and async/await:</strong> This is optional and not a strict requirement, but will be helpful.</p>
</li>
</ol>
<h2 id="heading-what-are-synchronous-and-asynchronous-code">What are Synchronous and Asynchronous Code?</h2>
<p>When writing code for Node.js applications, there are two different ways it can run: synchronous (sync) and asynchronous (async). Synchronous code is referred to as <em>blocking</em> because when it runs, no other code runs until execution is complete.</p>
<p>An analogy for this is a busy restaurant. Picture a waiter who refuses to wait on other tables until the table they’re presently serving has received their orders and has started eating. While the food is being prepared, the waiter waits around doing nothing and only approaches your table to take your order when they are completely finished with the previous table. Needless to say, the waiter may not receive a great tip for that service.</p>
<p>This is what synchronous code is. It halts the execution of other processes until it’s complete. You can see how it works in the example below:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> syncWaiter = <span class="hljs-function">(<span class="hljs-params">name</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`<span class="hljs-subst">${name}</span> attends to tables pretty slowly.`</span>);
};

syncWaiter(<span class="hljs-string">"Devin"</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"At least all the orders are correct!"</span>);
</code></pre>
<p>The code above will be run in sequence, in the order it appears.</p>
<p>Asynchronous code, unlike synchronous code, doesn’t halt all other processes until one task is executed – rather, it proceeds to carry out other tasks while a longer process runs in the background.</p>
<p>Using our waiter analogy, in this case the waiter in the restaurant would go take an order from one table, pass the order to the kitchen, and while it’s being prepared, proceeds to your table to take your order as well. This way, the waiter is able to ensure different processes are started even if one process takes a bit longer than the rest. Check out the example below:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> asyncWaiter = <span class="hljs-function">(<span class="hljs-params">name</span>) =&gt;</span> {
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">()=&gt;</span> {<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`<span class="hljs-subst">${name}</span> attends to tables pretty quickly.`</span>)}, <span class="hljs-number">3000</span>)
};

asyncWaiter(<span class="hljs-string">"James"</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Wow! All the tables are attended to in a short time."</span>);
</code></pre>
<p>Unlike synchronous code, this code does run the function <code>asyncWaiter()</code> – but the callback inside the function executes later. When the duration elapses, the result is then shown on the screen. This is why asynchronous programs are referred to as <em>non-blocking.</em> They don’t halt the program, but move from one available task to another.</p>
<p>The code above returns the following:</p>
<pre><code class="lang-bash">Wow! All the tables are attended to <span class="hljs-keyword">in</span> a short time.
James attends to tables pretty quickly.
</code></pre>
<p>This print order happens because of how the <em>event loop</em> manages tasks: the synchronous <code>console.log()</code> that comes after <code>asyncWaiter()</code> runs immediately, while the asynchronous callback inside <code>asyncWaiter()</code> (from <code>setTimeout</code>) is scheduled to run later. If you don’t understand this just yet, don’t worry as I’ll break it down in detail shortly.</p>
<h2 id="heading-what-concurrency-and-parallelism-mean">What Concurrency and Parallelism Mean</h2>
<p>Node.js is single-threaded but often gives the appearance of a multi-threaded environment due to how it handles concurrency and parallelism. A thread is a single sequence of instructions executed by the CPU independently. Think of it like a single waiter named James in a restaurant.</p>
<p>If James handles multiple tasks around the same time and quickly, an onlooker outside the restaurant who sees the number of customers moving in and out of the restaurant may assume that there are a ton of waiters serving tables. In reality, James just handles his tasks asynchronously.</p>
<p>Before grasping the concept of the event loop, it’s good to understand what concurrency and parallelism are, as they help explain this.</p>
<h3 id="heading-concurrency-in-nodejs">Concurrency in Node.js</h3>
<p>Concurrency means having multiple processes run around the same time. In the waiter analogy, it is like James carrying out different tasks, though not simultaneously. He could, for instance take an order from a table and, while waiting for the food to arrive, request that extra salt be provided to another table. While the salt is on its way, he uses the waiting time to read the bill to a third table.</p>
<p>The key idea is that James never sits idle — he works on other tasks while waiting for one to finish. If this sounds an awful lot like asynchronous programming, it is because asynchronous code is just one way to achieve concurrency.</p>
<p>Other ways to execute concurrency are <a target="_blank" href="https://www.freecodecamp.org/news/multithreading-for-beginners/">multithreading</a> on a single CPU core and <a target="_blank" href="https://www.freecodecamp.org/news/how-to-handle-concurrency-in-go/">coroutines</a> which are just functions that pause their execution to resume at a later time.</p>
<h3 id="heading-parallelism-in-nodejs">Parallelism in Node.js</h3>
<p>Parallelism, on the other hand, also means having several tasks run at the same time – but instead of the tasks just being processed around the same time, they are executed at exactly the same time, simultaneously. In this case, the restaurant manager decides to hire multiple waiters and each table has a waiter who is taking orders at exactly the same time.</p>
<p>Parallelism can be achieved using multithreading on multiple CPU cores. In this setup, the threads share the same memory and run simultaneously while using clusters which run independently – each with its own memory space. Here’s a clear example of parallelism using the <code>worker_threads</code> module:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { Worker}  = <span class="hljs-built_in">require</span>(<span class="hljs-string">'worker_threads'</span>);

<span class="hljs-keyword">new</span> Worker(<span class="hljs-string">'./worker.js'</span>);
<span class="hljs-keyword">new</span> Worker(<span class="hljs-string">'./worker.js'</span>);
<span class="hljs-keyword">new</span> Worker(<span class="hljs-string">'./worker.js'</span>);

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Main thread keeps running in the process..."</span>);
</code></pre>
<p>The code above creates three worker threads in parallel on a multi-core machine. This doesn’t stop the main thread which continues to run, allowing each worker thread independently do its task. <code>worker.js</code> could be a simple file carrying out any task. In this case, it simply logs a message to the screen:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">"This worker thread is running here!"</span>);
</code></pre>
<p>Note that the argument for the <code>Worker</code> constructor can be any file path, and the order in which they are executed isn’t dependent on the order they appear in code. Each worker runs independently of the others and they run in parallel.</p>
<p>Concurrency and parallelism allow Node.js (which is single-threaded) to appear to manage multiple tasks simultaneously. Understanding these concepts sets the stage for the event loop, showing how Node.js manages to give the appearance of concurrency while still executing code in a single-threaded environment.</p>
<h2 id="heading-what-is-the-event-loop">What is the Event Loop?</h2>
<p>The event loop listens for events in the Node.js environment. It essentially listens for actions and then processes tasks or outputs values.</p>
<p>To better understand how this works, you can picture the Node.js environment as a fast-paced organization and the event loop as an overworked manager who refuses to hire a personal assistant. The manager oversees the operations of the entire office, and has a dedicated desk that contains whatever they are working on at that particular time. Let’s call this desk <em>the call stack</em>.</p>
<p>The call stack consists of whatever processes or tasks that Node.js is currently working on. When input is entered or code is written to do something, it gets moved to the call stack and from there gets executed.</p>
<p>The order in which this execution takes place is important, as synchronous code makes it to the call stack before asynchronous code. What happens to the asynchronous code you may ask? It goes into something known as the callback queue first before ending up on the call stack.</p>
<p>The callback queue is a lineup of asynchronous tasks that make it to the call stack only if the stack is empty. You can think of it like a file cabinet in the office, where asynchronous code that is processed by a specialized team of workers under the manager go to stay until the manager’s desk is cleared. The manager only heads to the cabinet when they’re done handling all the synchronous task on the call stack. This specialized team that handles asynchronous code like callbacks and async/await are the Node APIs or the Web APIs.</p>
<p>Node or Web APIs process asynchronous code. When the code comes in, it’s processed here and then placed in the callback queue for the event loop to pick up and take to the call stack. But there are some asynchronous tasks that are prioritized. These are known as microtasks, such as <a target="_blank" href="https://www.freecodecamp.org/news/guide-to-javascript-promises/">promises</a>.</p>
<p>Microtasks are given particular priority and are queued in a special microtask queue. This is usually checked after an operation before checking the callback queue. If nothing is present, the event loop checks the callback queue but if some task exists such as <code>process.nextTick()</code>, it gets handled immediately. Macrotasks consist of tasks that are regularly scheduled and handled by the event loop only after the microtasks are treated, such as <code>setTimeout()</code> and <code>setInterval()</code><em>.</em></p>
<p>So as you can see, the event loop is basically what it sounds like – a loop. It looks through events and handles tasks based on a prioritized schedule.</p>
<p>One thing to note, though, is that even within callback queues and microtask queues, there are phases. The event loop, for instance, must handle certain tasks before others even within the same category. This is where the phases of the event loop come in.</p>
<h2 id="heading-the-phases-of-the-event-loop">The Phases of the Event Loop</h2>
<p>By analogy, the event loop is akin to a manager who checks the status of projects and tasks at regular intervals. In this case, they have a specific schedule for checking the status of projects. Some projects or tasks take priority over others, and the manager has to look through them in a set order.</p>
<p>You can also visualize event loop phases as a train moving from station to station. It starts from one location and moves to others in a particular order until it’s complete, then starts the journey again. This arrangement determines what tasks get executed before others.</p>
<p>Here are the phases of the event loop in order:</p>
<ol>
<li><p>The timers phase: This phase executes the <code>setTimeout()</code> and <code>setInterval()</code> callbacks after the duration is run. The event loop starts here, like the first station on a train’s journey.</p>
</li>
<li><p>The pending callbacks phase: These are system-level callbacks, checked after the timers phase operations.</p>
</li>
<li><p>The poll phase: This phase handles input/output (I/O) events and executes the callbacks. In the absence of callbacks, the event loop waits for new ones here.</p>
</li>
<li><p>The check phase: This phase executes <code>setImmediate()</code> callbacks.</p>
</li>
<li><p>Close callbacks: This phase is concerned with executing close events like socket closes.</p>
</li>
</ol>
<p>These callback events are checked in order and run accordingly, so that if <code>setTimeout()</code> and <code>setImmediate()</code> are in the same code, <code>setTimeout()</code> runs first unless the “train” is say, in the Poll Phase of the loop so that <code>setImmediate()</code> runs before <code>setTimeout()</code>.</p>
<p>You can see this illustrated with the example below:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

fs.readFile(<span class="hljs-string">'trainMap.txt'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Train takes off"</span>);
    }, <span class="hljs-number">0</span>);
    setImmediate(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Oops! Immediate halt! There's a cat on the tracks!"</span>);
    })
});
</code></pre>
<p>You see in the code above that the callbacks are handled asynchronously. Recall that the event loop waits for new callbacks in the poll phase. What this means is that since <code>fs.readfile()</code> is a callback, it gets processed in the poll phase.</p>
<p><code>setTimeout()</code> is set to run in the timers phase but the event loop proceeds to the check phase (which comes next) where <code>setImmediate()</code> is executed. This is why <code>setImmediate()</code> runs before <code>setTimeout()</code> in this case. The event loop then continues from the check phase to the close phase, back to the timers phase, repeating this cycle continuously.</p>
<p>This explains why you see the output below printed to the screen:</p>
<pre><code class="lang-bash">Oops! Immediate halt! There<span class="hljs-string">'s a cat on the tracks!
Train takes off</span>
</code></pre>
<p>This illustrates how the event loop enforces order of execution across the different phases, ensuring that asynchronous operations run in the correct sequence.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The Node.js event loop can sometimes appear mysterious, but it really isn’t as complex as it first seems. At its core, it really is just the engine that ensures JavaScript can handle multiple tasks without freezing.</p>
<p>In this article, you’ve learnt about synchronous and asynchronous code, concurrency, parallelism, and how these concepts help explain the event loop and the phases of the event loop. Understanding how they work gives you the confidence to write asynchronous code without fear, debug more efficiently, and appreciate the power behind Node.js’s ability to handle concurrent tasks.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
