Top Node.js Interview Questions

author image Hirely
at 08 Jan, 2025

Question: What are callbacks in Node.js?

Answer:

In Node.js, callbacks are functions that are passed as arguments to other functions and are executed after the completion of an asynchronous operation. The callback function is called (or “invoked”) when the operation finishes, allowing Node.js to handle tasks asynchronously without blocking the main thread. This is an essential concept in Node.js, which is designed around asynchronous I/O to maintain non-blocking performance.

How Callbacks Work in Node.js:

  1. Asynchronous Nature: Node.js relies heavily on asynchronous operations (e.g., reading files, making HTTP requests, querying a database). Instead of blocking the execution while waiting for these tasks to complete, Node.js initiates the operation and continues executing the rest of the code. When the operation is finished, Node.js invokes the corresponding callback to handle the result.

  2. Callback Syntax: Callbacks are typically functions that take one or more arguments, with the first argument often reserved for an error (if any occurs), and subsequent arguments for the result of the operation.

    Example of a callback with an error-first pattern:

    const fs = require('fs');
    
    fs.readFile('example.txt', 'utf8', (err, data) => {
        if (err) {
            console.error('Error reading file:', err);
            return;
        }
        console.log('File content:', data);
    });

    In this example:

    • The readFile function takes a callback as its third argument.
    • If an error occurs during the file reading operation, it is passed as the first argument (err) to the callback.
    • If the operation is successful, the file content is passed as the second argument (data).
  3. Error-First Callback Pattern:

    • In Node.js, the error-first callback pattern is commonly used. This means the first parameter of a callback is always reserved for the error object (if any).
    • If the operation completes successfully, the error parameter is null or undefined. If there’s an error, it contains an error message or object, allowing the developer to handle errors efficiently.
  4. Non-blocking Execution:

    • Callbacks enable non-blocking behavior in Node.js. When an asynchronous function is called, it doesn’t wait for the task to complete. Instead, it registers the callback and moves on to the next task.
    • Once the task finishes, the callback is executed to process the result.

Example:

Here’s a simple example where a callback is used to handle the result of an asynchronous function:

// Simulating an asynchronous task using setTimeout
function asyncTask(callback) {
    setTimeout(() => {
        console.log('Task completed');
        callback();  // Calling the callback when the task is done
    }, 2000);  // 2 seconds delay
}

function onTaskComplete() {
    console.log('Callback function executed!');
}

// Initiating the asynchronous task with the callback
asyncTask(onTaskComplete);

Explanation:

  • The asyncTask function simulates an asynchronous operation using setTimeout (with a 2-second delay).
  • The onTaskComplete function is passed as a callback to be executed when the task is finished.
  • Even though setTimeout is asynchronous, Node.js doesn’t block the main execution. After 2 seconds, the callback (onTaskComplete) is invoked.

Common Use Cases for Callbacks in Node.js:

  1. File System Operations: Node.js’s fs module (for file handling) relies heavily on callbacks for asynchronous I/O operations.

    Example:

    const fs = require('fs');
    
    fs.readFile('file.txt', 'utf8', (err, data) => {
        if (err) {
            console.error('Error reading file:', err);
        } else {
            console.log('File content:', data);
        }
    });
  2. HTTP Requests: When making HTTP requests, callbacks are used to handle responses once the request is complete.

    Example using the http module:

    const http = require('http');
    
    http.get('http://example.com', (response) => {
        let data = '';
        
        response.on('data', (chunk) => {
            data += chunk;
        });
        
        response.on('end', () => {
            console.log('Response received:', data);
        });
    }).on('error', (err) => {
        console.log('Error:', err.message);
    });
  3. Event Emitters: Callbacks are also used in event-driven programming with event emitters, where specific actions are triggered in response to emitted events.

Benefits of Callbacks in Node.js:

  • Asynchronous Execution: Callbacks allow Node.js to perform tasks asynchronously, which is crucial for I/O-heavy applications like web servers.
  • Non-blocking: The program doesn’t stop and wait for the task to finish; it continues processing other tasks while waiting for the callback to be triggered.
  • Error Handling: The error-first pattern in callbacks ensures that errors are handled effectively and consistently.

Potential Drawbacks:

  • Callback Hell (Pyramid of Doom): When dealing with multiple nested asynchronous calls, the code can become difficult to read and maintain, leading to what’s known as “callback hell.” To address this, developers often use techniques like promises and async/await to improve readability.

Conclusion:

Callbacks are a key feature of Node.js, enabling the non-blocking, asynchronous execution that is central to its performance model. They allow for efficient handling of I/O operations and other asynchronous tasks, but require careful error handling and management of nested functions to avoid complexity.

Question: What are Promises in Node.js?

Answer:

In Node.js, Promises are a modern approach to handling asynchronous operations, providing a cleaner and more manageable alternative to traditional callback-based patterns. A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises allow developers to write asynchronous code in a more readable and maintainable manner, avoiding the problems commonly associated with callbacks, such as “callback hell.”

Key Concepts of Promises:

A Promise has three possible states:

  1. Pending: The initial state, where the asynchronous operation has not yet completed.
  2. Fulfilled (Resolved): The state when the asynchronous operation has completed successfully, and the Promise has a resulting value.
  3. Rejected: The state when the asynchronous operation fails, and the Promise is provided with an error or reason for the failure.

A Promise is either fulfilled or rejected, but it can never transition back to pending once it has been resolved or rejected.

How Promises Work:

Promises allow asynchronous code to be written in a more synchronous-like fashion using .then() and .catch() methods, making it easier to chain multiple asynchronous operations together. Promises help prevent “callback hell” by providing a more structured approach to handling asynchronous results.

Creating a Promise:

A Promise is created using the new Promise() constructor, where you define the logic of the asynchronous operation inside a function that takes two arguments: resolve (for fulfillment) and reject (for failure).

const myPromise = new Promise((resolve, reject) => {
    let success = true;

    if (success) {
        resolve("Operation successful!");
    } else {
        reject("Operation failed.");
    }
});
  • If the operation is successful, you call resolve() with the result.
  • If the operation fails, you call reject() with an error or failure message.

Handling Promises with .then() and .catch():

After creating a Promise, you can handle its fulfillment or rejection using the .then() and .catch() methods.

  1. .then(): This method is used to handle the successful result of the Promise once it’s fulfilled.
  2. .catch(): This method is used to handle errors if the Promise is rejected.

Example:

const myPromise = new Promise((resolve, reject) => {
    let success = true;

    if (success) {
        resolve("Operation successful!");
    } else {
        reject("Operation failed.");
    }
});

myPromise
    .then((result) => {
        console.log(result);  // "Operation successful!"
    })
    .catch((error) => {
        console.log(error);   // "Operation failed."
    });
  • The .then() method is called when the Promise is fulfilled, and the resulting value is passed to it.
  • The .catch() method is called if the Promise is rejected, and the error message is passed to it.

Chaining Promises:

One of the main advantages of Promises is the ability to chain multiple .then() handlers, which is especially useful when you have multiple asynchronous operations that depend on each other.

Example:

const doTask1 = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("Task 1 completed"), 1000);
    });
};

const doTask2 = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("Task 2 completed"), 1000);
    });
};

doTask1()
    .then((result1) => {
        console.log(result1); // "Task 1 completed"
        return doTask2();     // Return the next promise
    })
    .then((result2) => {
        console.log(result2); // "Task 2 completed"
    })
    .catch((error) => {
        console.log("Error:", error);
    });

In this example:

  • The second task (doTask2) is executed only after the first task (doTask1) is fulfilled.
  • This chaining of promises allows each asynchronous operation to proceed in sequence, making the code more readable than nested callbacks.

Async/Await (Syntactic Sugar for Promises):

async and await are newer features in JavaScript that make working with Promises even more readable by allowing asynchronous code to be written in a synchronous style.

  • async: Declares a function as asynchronous. It ensures that the function always returns a Promise.
  • await: Pauses the execution of an async function until the Promise is fulfilled or rejected, allowing you to work with asynchronous code as if it were synchronous.

Example using async/await:

const doTask1 = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("Task 1 completed"), 1000);
    });
};

const doTask2 = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("Task 2 completed"), 1000);
    });
};

const runTasks = async () => {
    const result1 = await doTask1();
    console.log(result1); // "Task 1 completed"
    
    const result2 = await doTask2();
    console.log(result2); // "Task 2 completed"
};

runTasks().catch((error) => {
    console.log("Error:", error);
});
  • await waits for the doTask1() promise to resolve before moving on to the next line of code, making the code flow appear more synchronous.

Benefits of Promises:

  • Cleaner and more readable code: Promises allow chaining of multiple asynchronous operations, making the code easier to follow and maintain.
  • Error handling: The .catch() method provides a centralized way to handle errors, making it easier to manage exceptions compared to callbacks.
  • Avoid Callback Hell: With Promises, you can avoid nested callback functions, which makes the code much more readable and less prone to errors.

Drawbacks:

  • Still a bit verbose: Although Promises are cleaner than callbacks, they can still be verbose, especially with complex error handling in deeply nested chains.
  • Not automatically synchronous: Promises do not inherently make code synchronous. They still involve asynchronous processing, but they allow it to be written in a more linear and structured way.

Conclusion:

Promises in Node.js provide a structured, readable way to handle asynchronous operations. They allow for clean, chainable code that avoids the pitfalls of callback-based approaches like “callback hell.” With Promises, you can manage asynchronous results and errors more easily, and with the introduction of async/await, working with Promises has become even simpler and more intuitive.

Read More

If you can’t get enough from this article, Aihirely has plenty more related information, such as Node.js interview questions, Node.js interview experiences, and details about various Node.js job positions. Click here to check it out.

Related Posts

Trace Job opportunities

Hirely, your exclusive interview companion, empowers your competence and facilitates your interviews.

Get Started Now