Amblem
Furkan Baytekin

JavaScript’s Event Loop and Asynchronous Execution

How JavaScript runtimes enable async execution and handle concurrency

JavaScript’s Event Loop and Asynchronous Execution
153
5 minutes

JavaScript is often described as “single-threaded” and “asynchronous,” but what does that actually mean? How does JavaScript handle multiple tasks without blocking execution? In this post, we’ll break down how JavaScript executes code, what the event loop does, and how different environments like browsers, Node.js, Bun, and Deno manage async tasks.

Is JavaScript Single or Multi-Threaded?

JavaScript itself is single-threaded, meaning it can execute only one piece of code at a time in a single call stack. However, JavaScript can handle concurrency through asynchronous programming. This is possible because JavaScript doesn’t execute all tasks by itself—it delegates certain operations to the underlying runtime environment.

JavaScript’s Execution Model

JavaScript follows a run-to-completion model, meaning that once a function starts executing, nothing else can interrupt it until it finishes.

The Role of the JavaScript Engine

A JavaScript engine (e.g., V8 for Chrome and Node.js, SpiderMonkey for Firefox) executes JavaScript code. The engine itself:

  1. Parses and compiles JavaScript into machine code.
  2. Executes synchronous JavaScript line by line.
  3. Interacts with the event loop to handle async tasks.

However, the engine does not handle async tasks like timers, HTTP requests, or file system operations—that’s where the runtime environment comes in.

What is a JavaScript Runtime Environment?

A JavaScript runtime environment provides additional APIs that JavaScript itself does not have. These environments include:

Each environment includes:

How the JavaScript Event Loop Works

The event loop is what enables JavaScript to handle asynchronous tasks while maintaining its single-threaded nature.

Event Loop Components

  1. Call Stack
  1. Web APIs (in Browsers) / System APIs (in Node.js, Bun, Deno)
  1. Callback Queue
  1. Microtask Queue

Step-by-Step Execution

  1. JavaScript starts executing synchronous code in the call stack.
  2. If an asynchronous task (like setTimeout) is encountered, it’s delegated to the runtime (browser or Node.js).
  3. Once the async task is complete, its callback is added to the appropriate queue:
  1. The event loop checks the call stack:
  1. The process repeats indefinitely.

Asynchronous Examples

Example 1: setTimeout

js
console.log("Start"); setTimeout(() => { console.log("Timeout callback"); }, 1000); console.log("End");

Execution Flow:

  1. "Start" is logged.
  2. setTimeout() is sent to the Web API (browser) or Timer API (Node.js).
  3. "End" is logged.
  4. After 1000ms, the callback "Timeout callback" is added to the event queue.
  5. Once the call stack is empty, the event loop moves the callback to the call stack.

Output:

Start End Timeout callback

Example 2: Promises (Microtasks)

js
console.log("Start"); setTimeout(() => console.log("Timeout callback"), 0); Promise.resolve().then(() => console.log("Promise resolved")); console.log("End");

Execution Flow:

  1. "Start" is logged.
  2. setTimeout() is sent to the Web API.
  3. Promise.resolve().then(...) is added to the microtask queue.
  4. "End" is logged.
  5. Microtasks run before the event queue, so "Promise resolved" is logged.
  6. The event loop picks up "Timeout callback" from the event queue.

Output:

Start End Promise resolved Timeout callback

JavaScript Runtime Environments

1. Browser Environment

2. Node.js

3. Bun

4. Deno


Conclusion

Understanding how JavaScript handles concurrency is crucial for writing efficient, non-blocking applications. Next time you’re working with async code, remember: JavaScript doesn’t do everything itself—it relies on the runtime environment!

The Most Helpful Video Contents



Album of the day:

Suggested Blog Posts