HTML & JS Topic

Node.js Asynchronous JavaScript

Understanding Asynchronous Programming in Node.js

Node.js is single-threaded but handles thousands of concurrent connections through non-blocking I/O and event-driven architecture.

Why Asynchronous?

  • Non-blocking: Don't wait for I/O operations to complete

  • High concurrency: Handle multiple operations simultaneously

  • Better performance: Efficient use of single thread

  • Scalability: Handle many connections with minimal resources

1. Callbacks

Callbacks are functions passed as arguments to other functions, to be executed later when an operation completes.

How Callbacks Work:

javascript

// Synchronous callback (executed immediately)
function syncCallback(callback) {
  console.log('Before callback');
  callback();
  console.log('After callback');
}

// Asynchronous callback (executed later)
function asyncCallback(callback) {
  console.log('Before callback');
  setTimeout(callback, 1000); // Execute after 1 second
  console.log('After callback');
}

syncCallback(() => console.log('Callback executed'));
// Output: Before callback → Callback executed → After callback

asyncCallback(() => console.log('Async callback executed'));
// Output: Before callback → After callback → (1 sec later) Async callback executed

Node.js Style Callbacks (Error-First Pattern):

Node.js convention: Callbacks take (error, result) as parameters.

javascript

const fs = require('fs');

// Read file with callback
fs.readFile('file.txt', 'utf8', (error, data) => {
  if (error) {
    console.error('Error reading file:', error);
    return;
  }
  console.log('File content:', data);
});

// Custom function with Node.js callback pattern
function divide(a, b, callback) {
  if (b === 0) {
    // Error-first: pass error as first argument
    callback(new Error('Cannot divide by zero'), null);
  } else {
    // Success: error is null, result is second argument
    callback(null, a / b);
  }
}

// Using the function
divide(10, 2, (error, result) => {
  if (error) {
    console.error('Error:', error.message);
  } else {
    console.log('Result:', result); // 5
  }
});

Callback Hell (Pyramid of Doom):

javascript

// Nested callbacks become hard to read and maintain
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
  if (err1) {
    console.error(err1);
    return;
  }
  
  fs.readFile('file2.txt', 'utf8', (err2, data2) => {
    if (err2) {
      console.error(err2);
      return;
    }
    
    fs.writeFile('combined.txt', data1 + data2, (err3) => {
      if (err3) {
        console.error(err3);
        return;
      }
      
      console.log('Files combined successfully');
    });
  });
});

2. Promises

Promises represent the eventual completion (or failure) of an asynchronous operation and its resulting value.

Promise States:

  1. Pending: Initial state, neither fulfilled nor rejected

  2. Fulfilled: Operation completed successfully

  3. Rejected: Operation failed

Creating Promises:

javascript

// Create a promise
const myPromise = new Promise((resolve, reject) => {
  // Asynchronous operation
  const success = true; // Simulate success/failure
  
  setTimeout(() => {
    if (success) {
      resolve('Operation successful!'); // Fulfill promise
    } else {
      reject(new Error('Operation failed!')); // Reject promise
    }
  }, 1000);
});

Using Promises:

javascript

// Consuming a promise
myPromise
  .then((result) => {
    console.log('Success:', result);
    return result.toUpperCase(); // Can return value for next .then()
  })
  .then((modifiedResult) => {
    console.log('Modified:', modifiedResult);
  })
  .catch((error) => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('Operation completed (success or failure)');
  });

Promise Methods:

1. Promise.resolve() & Promise.reject():

javascript

// Create already resolved/rejected promises
const resolvedPromise = Promise.resolve('Immediate value');
const rejectedPromise = Promise.reject(new Error('Immediate error'));

2. Promise.all(): Wait for all promises to resolve

javascript

const promise1 = Promise.resolve(3);
const promise2 = 42; // Non-promise values are converted
const promise3 = new Promise((resolve) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // [3, 42, 'foo'] after 100ms
  })
  .catch((error) => {
    // If ANY promise rejects, entire Promise.all rejects
  });

3. Promise.allSettled(): Wait for all promises to settle (resolve or reject)

javascript

Promise.allSettled([
  Promise.resolve(1),
  Promise.reject(new Error('Error')),
  Promise.resolve(3)
]).then((results) => {
  results.forEach((result) => {
    console.log(result.status); // "fulfilled" or "rejected"
  });
});

4. Promise.race(): Returns first settled promise (resolve or reject)

javascript

const fastPromise = new Promise((resolve) => {
  setTimeout(resolve, 100, 'Fast');
});

const slowPromise = new Promise((resolve) => {
  setTimeout(resolve, 500, 'Slow');
});

Promise.race([fastPromise, slowPromise])
  .then((value) => {
    console.log(value); // "Fast" (after 100ms)
  });

5. Promise.any(): Returns first fulfilled promise (ignores rejections)

javascript

const rejectedPromise = Promise.reject(new Error('Fail'));
const slowPromise = new Promise((resolve) => {
  setTimeout(resolve, 500, 'Slow');
});
const fastPromise = new Promise((resolve) => {
  setTimeout(resolve, 100, 'Fast');
});

Promise.any([rejectedPromise, slowPromise, fastPromise])
  .then((value) => {
    console.log(value); // "Fast"
  });

Converting Callbacks to Promises:

javascript

const fs = require('fs');
const util = require('util');

// Method 1: Using util.promisify
const readFilePromise = util.promisify(fs.readFile);

// Method 2: Manual conversion
function readFileAsync(path, encoding) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, encoding, (error, data) => {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

// Usage
readFileAsync('file.txt', 'utf8')
  .then(data => console.log(data))
  .catch(error => console.error(error));

3. Async / Await

Async/await is syntactic sugar over promises, making asynchronous code look synchronous.

Async Functions:

  • async function always returns a promise

  • await pauses execution until promise settles

  • Can use try/catch for error handling

javascript

// Basic async function
async function fetchData() {
  // await can only be used inside async functions
  const data = await someAsyncOperation();
  return data;
}

Example with Error Handling:

javascript

async function processFiles() {
  try {
    // Read first file
    const data1 = await readFileAsync('file1.txt', 'utf8');
    console.log('File 1:', data1);
    
    // Read second file (waits for first to complete)
    const data2 = await readFileAsync('file2.txt', 'utf8');
    console.log('File 2:', data2);
    
    // Process data
    const result = processData(data1, data2);
    return result;
    
  } catch (error) {
    // Handle any error in the try block
    console.error('Error processing files:', error);
    throw error; // Re-throw if needed
  }
}

// Calling async function (returns promise)
processFiles()
  .then(result => console.log('Success:', result))
  .catch(error => console.error('Failed:', error));

Parallel Execution with Async/Await:

javascript

async function parallelExecution() {
  try {
    // Start multiple promises in parallel
    const [user, posts, comments] = await Promise.all([
      fetchUser(userId),
      fetchPosts(userId),
      fetchComments(userId)
    ]);
    
    console.log('All data fetched:', { user, posts, comments });
    
  } catch (error) {
    console.error('Error in parallel execution:', error);
  }
}

// For-loop with async/await (sequential)
async function processItems(items) {
  const results = [];
  for (const item of items) {
    const result = await processItem(item); // Process one at a time
    results.push(result);
  }
  return results;
}

// For-loop with Promise.all (parallel)
async function processItemsParallel(items) {
  const promises = items.map(item => processItem(item));
  return await Promise.all(promises); // Process all at once
}

IIFE with Async/Await:

javascript

// Immediately Invoked Async Function Expression
(async () => {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
})();

4. Event Loop (Basic Understanding)

The Event Loop is what allows Node.js to perform non-blocking I/O operations despite being single-threaded.

Simplified Event Loop Architecture:

text

┌───────────────────────────┐
│         Call Stack        │ ← Executes synchronous code
└─────────────┬─────────────┘
              │
┌─────────────▼─────────────┐
│          Node APIs         │ ← Handles async operations (fs, http, timers)
└─────────────┬─────────────┘
              │
┌─────────────▼─────────────┐
│   Callback/Task Queue     │ ← Stores completed async callbacks
│   (Microtask Queue)       │ ← Stores promises (.then, .catch)
└─────────────┬─────────────┘
              │
┌─────────────▼─────────────┐
│        Event Loop         │ ← Moves callbacks to call stack when empty
└───────────────────────────┘

Event Loop Phases:

  1. Timers: Execute setTimeout() and setInterval() callbacks

  2. Pending Callbacks: Execute I/O callbacks deferred to next loop

  3. Idle, Prepare: Internal use

  4. Poll: Retrieve new I/O events; execute I/O callbacks

  5. Check: Execute setImmediate() callbacks

  6. Close Callbacks: Execute close events (socket.on('close', ...))

Example Demonstrating Event Loop:

javascript

console.log('1: Start');

setTimeout(() => {
  console.log('2: Timeout callback');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('3: Promise resolved');
  });

console.log('4: End');

// Output order:
// 1: Start
// 4: End
// 3: Promise resolved  ← Microtask (Promise) runs before Macrotask (setTimeout)
// 2: Timeout callback

Microtasks vs Macrotasks:

  • Microtasks: Promises, process.nextTick(), queueMicrotask()

  • Macrotasks: setTimeout, setInterval, setImmediate, I/O callbacks

Priority: Microtasks run after each macrotask, before next macrotask.

5. Blocking vs Non-blocking Code

Blocking (Synchronous) Code:

Code that stops execution until operation completes.

javascript

// Blocking example
const fs = require('fs');

console.log('Start reading file...');

// BLOCKING - stops execution until file is read
const data = fs.readFileSync('largefile.txt', 'utf8');
console.log('File content:', data.length, 'characters');

console.log('Program continues...'); // Won't run until file is read

Characteristics:

  • Simple to write and understand

  • Execution waits for operation to complete

  • Can cause performance issues

  • Not suitable for I/O operations in servers

Non-blocking (Asynchronous) Code:

Code that continues execution while operation runs in background.

javascript

// Non-blocking example
const fs = require('fs');

console.log('Start reading file...');

// NON-BLOCKING - continues execution immediately
fs.readFile('largefile.txt', 'utf8', (error, data) => {
  // This callback runs AFTER file is read
  console.log('File content:', data.length, 'characters');
});

console.log('Program continues immediately...'); // Runs immediately
// Output: Start reading file... → Program continues immediately... → File content...

// With async/await (non-blocking but looks synchronous)
async function readFileNonBlocking() {
  console.log('Start reading file...');
  
  // Doesn't block other operations
  const data = await fs.promises.readFile('largefile.txt', 'utf8');
  console.log('File content:', data.length, 'characters');
}

readFileNonBlocking();
console.log('This runs while file is being read');

Comparison Table:

Aspect Blocking Code Non-blocking Code
Execution Waits for operation Continues immediately
Performance Can be slow for I/O Efficient for I/O
Concurrency Poor (one at a time) Excellent (many at once)
Complexity Simple to write More complex patterns
Use Cases CLI tools, scripts Web servers, APIs, real-time apps
Error Handling Try/catch Callbacks, promises, async/await

Practical Example: Web Server

javascript

const http = require('http');

// BLOCKING server (BAD - blocks all requests)
const blockingServer = http.createServer((req, res) => {
  // This blocks the entire thread!
  const data = fs.readFileSync('largefile.txt', 'utf8');
  res.end(data); // No other requests can be processed until this completes
});

// NON-BLOCKING server (GOOD - handles concurrent requests)
const nonBlockingServer = http.createServer(async (req, res) => {
  // This doesn't block - other requests can be processed
  const data = await fs.promises.readFile('largefile.txt', 'utf8');
  res.end(data);
});

nonBlockingServer.listen(3000, () => {
  console.log('Server running on port 3000');
});

Choosing Between Approaches:

Use Blocking When:

  • Scripts/CLI tools where order matters

  • Configuration loading at startup

  • Simple, sequential operations

  • When simplicity is more important than performance

Use Non-blocking When:

  • Web servers/APIs

  • Database operations

  • File I/O in servers

  • Multiple concurrent operations

  • Real-time applications

Performance Impact Example:

javascript

// Simulating blocking vs non-blocking
function blockingOperation() {
  console.time('blocking');
  // Simulate CPU-intensive work
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  }
  console.timeEnd('blocking');
  return sum;
}

function nonBlockingOperation() {
  return new Promise((resolve) => {
    console.time('non-blocking');
    // Defer to next tick
    setImmediate(() => {
      let sum = 0;
      for (let i = 0; i < 1e9; i++) {
        sum += i;
      }
      console.timeEnd('non-blocking');
      resolve(sum);
    });
  });
}

// Blocking example
console.log('Before blocking');
blockingOperation(); // Blocks everything
console.log('After blocking'); // Only appears after operation

// Non-blocking example
console.log('Before non-blocking');
nonBlockingOperation().then(() => {
  console.log('Non-blocking completed');
});
console.log('After non-blocking init'); // Appears immediately

Understanding these asynchronous patterns is crucial for writing efficient, scalable Node.js applications!

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?