Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Asynchronous Control Flow in JavaScript

Tech 1

In single-threaded JavaScript, asynchronous execution is managed via callback functions. Consider how operating systems handle interrupts: while a peripheral device completes I/O, the CPU continues other work until an interrupt signals completion. Callbacks operate on a similar principle—the runtime registers a function to execute once an external operation finishes.

Example using setTimeout:

console.log("Initiating...");
setTimeout(() => {
  console.log("Deferred task");
}, 2000);
console.log("Terminating...");

Output sequence:

Initiating...
Terminating...
Deferred task

The arrow function passed to setTimeout registers a deferred callback. After approximately two seconds, the event loop pushes the callback onto the call stack. Because the operation is non-blocking, the interpreter proceeds immediately to the final log statement.

Event listeners and HTTP requests follow an identical pattern:

document.querySelector('#submit').addEventListener('click', (evt) => {
  evt.preventDefault();
  // handle interaction
});

fetch('/api/data')
  .then(resp => resp.json())
  .then(payload => console.log(payload));

When subsequent logic depends on prior asynchronous results, callbacks nest within callbacks:

console.log("Initiating...");
setTimeout(() => {
  console.log('Phase 1');
  setTimeout(() => {
    console.log('Phase 2');
  }, 1000);
}, 2000);
console.log("Terminating...");

This guarantees that "Phase 2" executes only after "Phase 1" completes. However, deeply nested clalbacks create the infamous "callback pyramid," rendering code brittle and difficult to reason about.

Illustrating the readability problem with a hypothetical workflow:

database.connect(config, (err, connection) => {
  if (err) return console.error(err);
  connection.query('SELECT id FROM users', (err, rows) => {
    if (err) return connection.release(), console.error(err);
    rows.forEach((row) => {
      filesystem.readFile(`./assets/${row.id}.json`, (err, buffer) => {
        if (err) return console.error(err);
        const metadata = JSON.parse(buffer);
        resizeAsset(metadata.path, (err, thumbnail) => {
          if (err) return console.error(err);
          cache.store(thumbnail, (err) => {
            if (err) console.error('Cache write failed:', err);
          });
        });
      });
    });
  });
});

Promises

ES6 standardized Promise objects to flatten asynchronous chains. The core concepts involve separating operation declaration from result handling and enabling sequential composition through chaining.

console.log("Initiating...");

const delay = (ms, value) => new Promise((fulfill) => {
  setTimeout(() => fulfill(value), ms);
});

console.log("Operations queued...");

delay(1000, 'Alpha')
  .then(result => {
    console.log(`First: ${result}`);
    return delay(5000, 'Beta');
  })
  .then(result => {
    console.log(`Second: ${result}`);
  });

console.log("Terminating...");

Execution yields:

Initiating...
Operations queued...
Terminating...
First: Alpha
Second: Beta

Each then handler returns a new Promise, allowing vertical rather than horizontal growth. The first delay resolves after one second; the second resolves five seconds later.

For parallel execution, Promise.all aggregates multiple asynchronous tasks:

const remoteCall = fetch('/api/config').then(r => r.json());
const localConstant = Promise.resolve(42);
const timeoutValue = new Promise((resolve) => {
  setTimeout(resolve, 100, 'dynamic');
});

Promise.all([remoteCall, localConstant, timeoutValue])
  .then(([config, constant, dynamic]) => {
    console.log({ config, constant, dynamic });
  });

Conversely, Promise.race settles as soon as the first participant resolves or rejects:

const slow = new Promise((resolve) => setTimeout(resolve, 800, 'tortoise'));
const fast = new Promise((resolve) => setTimeout(resolve, 100, 'hare'));

Promise.race([slow, fast]).then((winner) => {
  console.log(winner);  // "hare"
});

Async and Await

Biulding on generator functions introduced in ES6, ECMAScript later standardized async and await as syntactic sugar over Promises. This allows asynchronous code to read like synchronous routines.

const delay = (ms, value) => new Promise((resolve) => {
  setTimeout(() => resolve(value), ms);
});

async function executeWorkflow() {
  console.log("Initiating...");
  await delay(3000, 'checkpoint');
  console.log("Checkpoint reached");
  console.log("Finishing...");
}

executeWorkflow();

Without await, "Finishing..." would log before the deferred value. Inside an async function, however, await pauses execution until the Promise settles, yielding:

Initiating...
Checkpoint reached
Finishing...

Key constraints:

  • The await keyword is legal only within async functions.
  • Every async function implicitly returns a Promise, even if the return value is a primitive.
  • Sequential await statements execute in series; avoid unnecessary serialization of independent operations to prevent performance degradation.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.