Module 1: JavaScript Prerequisites for Backend

Modern ES6+ Features

These language features reduce boilerplate, prevent runtime exceptions, and simplify payload manipulation in server environments.

Destructuring

Destructuring unpacks values from arrays or properties from objects into distinct variables. This is the standard way to extract data from request bodies, parameters, and environment configurations.

JavaScript

// Object Destructuring with default values and aliasing
const req = {
  body: { username: 'alex', role: 'editor' },
  headers: { 'x-auth-token': 'xyz123' }
};

const { username, role, status = 'active' } = req.body;
const { 'x-auth-token': authToken } = req.headers; // Renamed to valid identifier

// Array Destructuring (often used with Promise.all results or tuples)
const coordinates = [37.7749, -122.4194];
const [latitude, longitude] = coordinates;

Rest & Spread Operators (...)

The syntax is identical, but the behavior depends on the context:

  • Rest: Gathers multiple elements into a single array/object. Used in function parameters or destructuring remainder values.
  • Spread: Unpacks elements of an array or properties of an object into a new container.

JavaScript

// Rest: Collect remaining payload fields while stripping sensitive data
const userRecord = { id: 101, passwordHash: 'secret', email: 'user@test.com', name: 'Sam' };
const { passwordHash, ...publicProfile } = userRecord; 
// publicProfile contains { id: 101, email: 'user@test.com', name: 'Sam' }

// Spread: Immutably update database records or configs
const defaultSettings = { timeout: 5000, retries: 3, logging: true };
const userSettings = { retries: 5 };
const finalConfig = { ...defaultSettings, ...userSettings, timestamp: Date.now() };

Template Literals

Defined by backticks (`), template literals enable multiline strings and variable interpolation via ${expression}.

JavaScript

const table = 'users';
const limit = 10;
const query = `
  SELECT id, email, created_at
  FROM ${table}
  WHERE status = 'active'
  LIMIT ${limit};
`;

Optional Chaining (?.)

Safely accesses nested object properties without manually verifying each link in the chain. If a reference evaluates to null or undefined, the expression short-circuits to undefined instead of throwing a TypeError: Cannot read properties of undefined.

JavaScript

const request = {
  user: {
    profile: {
      getDisplayName: () => 'JohnDoe'
    }
  }
};

// Deep property access
const street = request.user?.address?.street; // undefined (no error thrown)

// Method execution
const name = request.user?.profile?.getDisplayName?.(); // 'JohnDoe'

// Dynamic indexing
const firstRole = request.user?.roles?.[0]; // undefined

Nullish Coalescing (??)

A logical operator that returns its right-hand operand when its left-hand operand is only null or undefined. This fixes the pitfall of the logical OR (||), which falls back on any falsy value (0, "", false, NaN).

JavaScript

const serverConfig = {
  port: 0,              // Valid port: 0 allows the OS to assign an ephemeral port
  debugMode: false,     // Explicitly disabled
  timeout: null
};

// Using || (incorrect fallback on falsy values)
const portOR = serverConfig.port || 3000;         // Evaluates to 3000
const debugOR = serverConfig.debugMode || true;    // Evaluates to true

// Using ?? (strictly checks for null or undefined)
const portNC = serverConfig.port ?? 3000;         // 0
const debugNC = serverConfig.debugMode ?? true;    // false
const timeoutNC = serverConfig.timeout ?? 5000;    // 5000

Asynchronous JavaScript

Node.js runs single-threaded code alongside a non-blocking background architecture. Mastering asynchronous execution models prevents blocked threads and unhandled rejections.

Callbacks & Callback Hell

The original Node.js pattern used error-first callbacks: callback(err, data). When operations depended on sequentially prior operations, code nested inward uncontrollably, making error handling and control flow difficult to maintain.

JavaScript

// Anti-pattern: Callback Hell
getUser(userId, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    processPayment(orders[0], (err, receipt) => {
      if (err) return handleError(err);
      sendEmail(user.email, receipt, (err) => {
        if (err) return handleError(err);
        // Completed
      });
    });
  });
});

Promises

A Promise represents an operation that hasn’t completed yet but will produce a value (fulfilled) or a reason for failure (rejected) in the future.

  • Pending: Initial state; neither fulfilled nor rejected.
  • Fulfilled: Operation completed successfully (resolve()).
  • Rejected: Operation failed (reject()).

JavaScript

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    database.find({ id: userId }, (err, result) => {
      if (err) return reject(new Error('Database query failed'));
      resolve(result);
    });
  });
}

// Chaining avoids nesting
fetchUserData(101)
  .then((user) => fetchOrders(user.id))
  .then((orders) => processPayment(orders[0]))
  .then((receipt) => sendConfirmation(receipt))
  .catch((err) => console.error('Pipeline error:', err.message))
  .finally(() => cleanupResources());

async / await and try...catch

Syntactic sugar over Promises that allows asynchronous code to be written and read like synchronous code.

  • Marking a function async ensures it always returns a Promise.
  • The await keyword pauses execution of that specific function until the Promise settles, freeing the event loop to run other tasks.
  • Unhandled rejections inside an async function are caught using a standard try...catch block.

JavaScript

async function handleCheckout(userId) {
  try {
    const user = await fetchUserData(userId);
    const orders = await fetchOrders(user.id);
    
    if (!orders.length) {
      throw new Error('No orders found to process');
    }

    const receipt = await processPayment(orders[0]);
    await sendConfirmation(receipt);

    return { success: true, receiptId: receipt.id };
  } catch (error) {
    // Single consolidated error handler
    logger.error(`Checkout failed for user ${userId}:`, error);
    throw error; // Re-throw or map to an HTTP error response
  } finally {
    // Executes regardless of success or failure
    await releaseDbConnection();
  }
}

Parallel Asynchronous Execution

Do not await independent operations sequentially. Run them concurrently using Promise combinators:

JavaScript

// Slower: Sequential execution (~600ms total)
// const user = await fetchUser();     // 300ms
// const config = await fetchConfig(); // 300ms

// Faster: Parallel execution (~300ms total)
const [user, config] = await Promise.all([
  fetchUser(),
  fetchConfig()
]);

// Handle mixed outcomes without failing the entire batch
const results = await Promise.allSettled([sendWebhookA(), sendWebhookB()]);
results.forEach((res) => {
  if (res.status === 'rejected') logError(res.reason);
});

Execution Context & JavaScript Runtime

Understanding how the engine processes memory, resolves identifiers, and manages execution phases prevents memory leaks, scope bugs, and race conditions.

Execution Context & Call Stack

An Execution Context is an environment where JavaScript code is evaluated and executed. It is created in two phases:

  1. Creation Phase: The engine allocates memory for variables and functions (hoisting), sets up the scope chain, and sets this.
  2. Execution Phase: Code is executed line by line; variables are assigned concrete values.

The Call Stack follows a Last-In, First-Out (LIFO) order to keep track of where the program is in execution:

JavaScript

function first() {
  second();
}
function second() {
  console.log('Running');
}

first();
// Call Stack Steps:
// 1. push global()
// 2. push first()
// 3. push second()
// 4. pop second() -> outputs "Running"
// 5. pop first()
// 6. pop global()

Scope

Scope determines the accessibility of variables and functions:

  • Global Scope: Accessible everywhere across the process (can lead to memory leaks if misused).
  • Function Scope: Variables declared with var are constrained to their enclosing function.
  • Block Scope: Variables declared with let and const are constrained within {} braces (loops, if statements).
  • Lexical Scope: Inner functions have physical access to variables declared in their outer parent scopes based on where they are written in the source code.

Closures

A closure is the combination of a function bundled together with references to its surrounding lexical environment. A closure gives an inner function access to an outer function’s scope even after the outer function has finished executing and returned.

In backend services, closures are commonly used to create private state, factory functions, and configured middleware:

JavaScript

// Rate limiter state encapsulated in a closure
function createRateLimiter(limit) {
  let requestCount = 0; // Private state, inaccessible from the outside

  return function (req, res, next) {
    requestCount++;
    if (requestCount > limit) {
      return res.status(429).json({ error: 'Rate limit exceeded' });
    }
    next();
  };
}

const apiLimiter = createRateLimiter(100);

The this Keyword

Unlike languages like Java or C++, this in standard JavaScript functions is determined by how the function is called, not where it is defined:

Invocation Patternthis Reference
Method Call (obj.method())References the parent object (obj).
Direct Call (func())In strict mode ('use strict'), it evaluates to undefined. In non-strict mode, it references global.
Explicit Binding (call, apply, bind)Explicitly set to the provided target object.
Constructor Call (new MyClass())References the newly instantiated instance.
Arrow Function (() => {})Lexical: retains the this value of its enclosing lexical context. Does not bind its own this.

JavaScript

class DatabaseService {
  constructor() {
    this.connectionString = 'postgres://localhost:5432/db';
  }

  connect() {
    // Problem: Standard callback creates its own `this` or evaluates to undefined in strict mode
    // setTimeout(function() { console.log(this.connectionString); }, 100); // TypeError

    // Solution 1: Arrow function retains parent scope's `this`
    setTimeout(() => {
      console.log('Connected to:', this.connectionString);
    }, 100);

    // Solution 2: Explicit binding
    // setTimeout(function() { ... }.bind(this), 100);
  }
}

JavaScript Runtime Model (Node.js)

The runtime execution flow coordinates multiple system components:

  1. V8 Engine: Evaluates the JavaScript code and maintains the Call Stack and Memory Heap (where objects, closures, and reference types are allocated).
  2. libuv: A C library that provides Node.js with its Event Loop and thread pool (default 4 threads), delegating asynchronous operations (file system, DNS, crypto, network sockets) directly to the OS kernel.
  3. Task Queues:
    • Microtask Queue: Highest priority. Processes process.nextTick() callbacks first, followed by resolved Promise callbacks (.then(), await resumes).
    • Macrotask Queue: Evaluates timers (setTimeout, setInterval), I/O callbacks, and setImmediate().

Whenever the Call Stack becomes completely empty, the runtime drains the entire Microtask Queue before proceeding to the next phase of the Event Loop. Blocking the Call Stack with heavy synchronous calculations prevents the queues from draining, freezing network response handling across the entire server.