Frontend Development 16 min read

Understanding JavaScript Asynchronous Mechanisms and the Event Loop

This article explains why JavaScript, despite being single‑threaded, needs asynchronous mechanisms such as the event loop, details macro‑ and micro‑tasks, compares browser and Node.js implementations, and demonstrates common pitfalls and best practices using callbacks, Promise, generator, and async/await patterns.

ByteFE
ByteFE
ByteFE
Understanding JavaScript Asynchronous Mechanisms and the Event Loop

JavaScript Asynchronous Principles

JavaScript runs on a single thread, meaning only one task can execute at a time; long‑running tasks block the whole page, causing unresponsiveness. Asynchronous mechanisms, implemented via the Event Loop , allow I/O‑bound operations to proceed without blocking the CPU.

JavaScript is single‑threaded; only one task runs at a time. Browsers can create additional threads with Web Worker , but workers cannot manipulate the DOM.

Event Loop

An event loop maintains a Task Queue that stores all pending events. The loop repeatedly takes events from the queue, pushes their callbacks onto the call stack, and executes them. Asynchronous APIs (e.g., timers, network requests, file I/O) place callbacks into the queue after completion.

During execution, function calls first enter the call stack; asynchronous APIs run the task, and when finished the callback moves to the task queue, then back to the call stack.

Task Types

The browser distinguishes two kinds of tasks: macro‑tasks and micro‑tasks, both generated via browser‑provided APIs.

Macro‑tasks (macrotask)

setTimeout

setInterval

setImmediate (Node‑only)

requestAnimationFrame (browser‑only)

I/O

UI rendering (browser‑only)

Micro‑tasks (microtask)

process.nextTick (Node‑only)

Promise

Object.observe

MutationObserver

Event Loop Diagram

JavaScript Asynchronous Programming

Browser‑side asynchronous development has evolved through four stages: callbacks, Promise, generator, and async/await.

Callbacks

Callbacks are simple but lead to tightly coupled code and "callback hell".
function red() { console.log('red') }
function green() { console.log('green') }
function yellow() { console.log('yellow') }
const light = (timer, light, callback) => {
  setTimeout(() => {
    switch(light) {
      case 'red': red(); break;
      case 'green': green(); break;
      case 'yellow': yellow(); break;
    }
    callback()
  }, timer)
}
const work = () => {
  task(3000, 'red', () => {
    task(1000, 'green', () => {
      task(2000, 'yellow', work)
    })
  })
}
work()

Promise

Promise was introduced to transform nested callbacks into chainable calls.
const promiseLight = (timer, light) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      switch(light) {
        case 'red': red(); break;
        case 'green': green(); break;
        case 'yellow': yellow(); break;
      }
      resolve()
    }, timer)
  })
}
const work = () => {
  promiseLight(3000, 'red')
    .then(() => promiseLight(1000, 'green'))
    .then(() => promiseLight(2000, 'yellow'))
    .then(work)
}

Generator

Generator functions can pause and resume execution, offering data transformation and error‑handling capabilities.
const generator = function*() {
  yield promiseLight(3000, 'red')
  yield promiseLight(1000, 'green')
  yield promiseLight(2000, 'yellow')
  yield generator()
}
const generatorObj = generator()
generatorObj.next()
generatorObj.next()
generatorObj.next()

async/await

async/await lets developers write asynchronous code with a synchronous style; generators are essentially syntactic sugar for async functions.
const asyncTask = async () => {
  await promiseLight(3000, 'red')
  await promiseLight(1000, 'green')
  await promiseLight(2000, 'yellow')
}
asyncTask()

Browser vs. Node.js Differences

Node 11.0.0 (excluding 11) and earlier processed all tasks in the main queue before handling micro‑tasks.

Node has six task queues: four main queues and two intermediate queues. See the translation of the Node event‑loop series and the official Node guide for details.

From Node 11 onward, the event loop behaves like browsers: after each main‑queue task, all pending micro‑tasks run before the next main‑queue task.

Example code demonstrates the different output ordering before and after Node 11.

setTimeout(() => {
  console.log("Timer 1")
  new Promise((resolve, reject) => { resolve() }).then(() => console.log("Microtask 1"))
}, 1000);

setTimeout(() => {
  console.log("Timer 2")
  new Promise((resolve, reject) => { resolve() }).then(() => console.log("Microtask 2"))
}, 1000);

Images show the execution order for Node < 11 and Node ≥ 11.

Bad Cases in Asynchronous Programming

Even experienced developers can misuse async/await, leading to bugs that are hard to locate.

Serial Execution of Independent Async Functions

When multiple async calls have no dependency, using sequential await forces them to run one after another, wasting time.

function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); }
async function main() {
  const start = console.time('async');
  await sleep(1000);
  await sleep(2000);
  const end = console.timeEnd('async'); // ~3s
}

Solution: use Promise.all or start promises without awaiting immediately.

async function main() {
  const start = console.time('async');
  await Promise.all([sleep(1000), sleep(2000)]);
  console.timeEnd('async'); // ~2s
}

Uncaught Errors

Errors thrown inside an async function that is returned without await bypass the surrounding try/catch .

async function err() { throw "error" }
async function main() {
  try { return err(); }
  catch (e) { console.log(e); }
}
main(); // error not caught

Fixes: await the call, or attach .catch to the returned promise, or use libraries like await-to-js .

Synchronous Thinking When Writing Async Code

Assuming async calls behave synchronously can cause race conditions, e.g., two UI‑color tasks where the later‑started task finishes first.

async function taskA() { return new Promise(resolve => setTimeout(() => { changePageColor('red'); resolve(); }, 500)); }
async function taskB() { return new Promise(resolve => setTimeout(() => { changePageColor('blue'); resolve(); }, 1000)); }
async function executeTask(t) { await t(); }
executeTask(taskB); // starts first
executeTask(taskA); // starts later but finishes earlier -> final color blue (wrong)

Solution: introduce a lock that records the intended color and only applies the change if the lock still matches.

let workingLock = false;
async function taskA() {
  return new Promise(resolve => {
    workingLock = 'red';
    setTimeout(() => { if (workingLock === 'red') changePageColor('red'); resolve(); }, 500);
  });
}
async function taskB() {
  return new Promise(resolve => {
    workingLock = 'blue';
    setTimeout(() => { if (workingLock === 'blue') changePageColor('blue'); resolve(); }, 1000);
  });
}
function changePageColor(color) { console.log(color); }
async function executeTask(t) { await t(); }
executeTask(taskB);
executeTask(taskA);

References

[1] "Understanding the Event Loop" – https://blog.csdn.net/weixin_52092151/article/details/119788483

[2] "What is JavaScript Generator and How to Use It" – https://zhuanlan.zhihu.com/p/45599048

[3] "Generator Functions Explained" – https://www.ruanyifeng.com/blog/2015/04/generator.html

[4] "Async Functions Explained" – https://www.ruanyifeng.com/blog/2015/05/async.html

[5] "[Translation] Node Event Loop Series – Timer, Immediate, nextTick" – https://zhuanlan.zhihu.com/p/87579819

[6] "The Node.js Event Loop, Timers, and process.nextTick()" – https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

[7] "await-to-js" – https://github.com/scopsy/await-to-js/blob/master/src/await-to-js.ts

frontendJavaScriptNode.jsAsyncevent-loopPromise
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.