Interview Question:
What does Asynchronous actually means ?
- This term means, if there are 10 statements/functions written in a code. There’s no guaranty / mandate to run these in-order; synchronously. There can be some statements/functions which will run after some execution or even between an execution. This behavior is called Asynchronous.
How NodeJS achieves this Async behavior ?
- To achieve this async behavior, NodeJS uses an
Event Loop
to track the executions in queues. If NodeJS comes across a function which is async in nature, it will delegate the task to the system’s kernel which involveslibuv
, and then from there event loop takes of the rest.
What is Asynchronous Programming?
- Definition: Asynchronous programming is a programming paradigm where tasks are executed without blocking the main thread. This allows other operations to continue while waiting for some tasks (e.g., network requests, I/O operations) to complete.
- Blocking vs Non-blocking: In synchronous (blocking) programming, tasks are executed one after another, meaning if one task takes time (like fetching data from a database), it blocks the execution of further tasks. In asynchronous programming, tasks can run in the background, allowing the main program to continue executing.
- Callback-driven: Asynchronous programming often uses callbacks, promises, or async/await mechanisms to handle tasks that do not complete immediately (e.g., fetching data, reading files, etc.).
Node.js Asynchronous Programming
Node.js is designed to handle asynchronous operations efficiently, making it perfect for I/O-heavy tasks (e.g., handling many concurrent requests). Key points:
1. Single-threaded Non-blocking I/O
- Single-threaded: Node.js uses a single-threaded event loop to handle all incoming tasks, meaning it doesn’t spawn a new thread for each request (unlike traditional multithreaded models like in Java or PHP).
- Non-blocking I/O: This allows Node.js to handle multiple operations concurrently without blocking other tasks. For example, if a file read operation takes time, Node.js doesn’t wait for it to complete and can handle other operations in the meantime.
2. The Event Loop
-
Definition: The event loop is at the heart of Node.js’s asynchronous behavior. It continuously checks for tasks to execute, like I/O callbacks or timers.
-
Task Queues: Tasks like I/O operations are moved to a task queue, where the event loop picks them up once they’re ready (e.g., data is returned, or a file is read).
-
Phases of the Event Loop: The event loop goes through phases such as timers, I/O callbacks, idle handlers, polling, and checking (for immediate execution).
-
When NodeJS initializes, it starts an event loop process. This process runs in a loop, which have different phases. Each phase have its own callback queue.
-
When scripts are executed all the synchronous code is executed by the V8 engine, and the I/O tasks are delegated to system’s kernel which involves
libuv
-
The event loop interacts with this sub-system and get the event when this I/O tasks is finished. Then it adds the associated callbacks of these I/O tasks in the callback queue.
-
The event loop also manages the execution of the callbacks from queue.
-
Event loop lives in the main thread only, and it executes these callbacks in between, which means it might pause some running synchronous task, execute the callback in between, and exactly this behavior makes NodeJS asynchronous in nature.
-
Phases of the Event Loop:
- Timers: Executes callbacks scheduled by
setTimeout()
andsetInterval()
. - Pending Callbacks: Handles I/O operations that have completed.
- Idle, Prepare: Internal tasks (usually ignored in most cases).
- Poll: Retrieves new I/O events (e.g., reading from sockets, files).
- Check: Executes
setImmediate()
callbacks. - Close Callbacks: Executes callbacks for closing operations (e.g.,
socket.on('close')
).
- Timers: Executes callbacks scheduled by
-
Example: The event loop ensures that a file reading operation (I/O) does not block the server’s ability to handle other requests.
-
There are phases in the Event Loop, like timers, poll, close callbacks, etc. These phases have there own callback queues and event loop goes into each phase. This is the reason, when a scheduled timer of 100ms might be executed after 105ms. Because the event loop might be in another phase and finishing its tasks.
3. Callbacks
-
Definition: A callback is a function passed as an argument to another function that will be executed once the asynchronous operation completes.
-
Example:
- Drawback: Heavy use of callbacks can lead to callback hell or the pyramid of doom, where multiple nested callbacks make code difficult to read and maintain.
4. Promises
-
Definition: Promises are an improvement over callbacks, offering a more structured way to handle asynchronous operations.
-
States:
Pending
: Initial state, neither fulfilled nor rejected.Fulfilled
: Operation completed successfully.Rejected
: Operation failed.
-
Example:
- Chaining: Promises allow chaining
.then()
for multiple asynchronous operations, which avoids deep nesting.
5. Async/Await
- Definition:
async/await
is syntactic sugar built on top of Promises. It makes asynchronous code look and behave like synchronous code, making it easier to read and write. - Example:
async
functions always return a Promise.await
pauses execution until the Promise is resolved or rejected, but it doesn’t block the main thread like synchronous code.
6. Concurrency and Parallelism in Node.js
- Concurrency: Node.js can handle multiple tasks concurrently using non-blocking I/O, but it can only process one operation at a time due to its single-threaded nature.
- Parallelism: While Node.js is single-threaded, it uses worker threads for computationally expensive tasks and background processes. With Node.js’s Worker Threads module or child processes, CPU-bound tasks can run in parallel.
7. Timers in Node.js
- Node.js provides built-in functions to handle asynchronous timers:
setTimeout()
: Executes a function after a specified delay.setInterval()
: Repeatedly executes a function with a specified delay between each call.- Example:
8. I/O Bound vs CPU Bound Operations
- I/O-bound: Node.js excels in handling I/O-bound tasks, such as database queries, network requests, and file system operations. Asynchronous I/O frees up the main thread to handle more requests while waiting for the I/O operations to finish.
- CPU-bound: Node.js struggles with CPU-bound tasks (e.g., intensive computations) because these tasks can block the single event loop thread, reducing its efficiency. Using worker threads or offloading to external services helps mitigate this issue.
9. Error Handling in Asynchronous Code
- Callbacks: Errors in callbacks are handled by passing the error object as the first argument (e.g.,
err
in thefs.readFile
example). - Promises: Errors are caught using
.catch()
method.- Example:
- Async/Await: Errors are handled using
try/catch
blocks insideasync
functions.- Example: