1. Fundamentals of JavaScript Asynchronicity
1.Understanding the JavaScript Event-Loop
- The event loop is a mechanism that allows JavaScript to handle asynchronous operations by managing the execution of multiple code blocks in a non-blocking manner.
Call Stack: The stack data structure that manages function execution.Web APIs: Browser-provided APIs for handling asynchronous tasks.Task Queue / Microtask Queue: Queues where asynchronous tasks wait to be executed.Event Loop in Action: The cycle that moves tasks from the queue to the stack.
2.Blocking VS Non-Blocking Code
- Synchronous vs Asynchronous Execution: Synchronous code runs sequentially, blocking further execution, while asynchronous code does not block execution.
- Why Asynchronous Code is Important in APIs: It allows API calls and other I/O operations to run in the background, improving performance.
- Asynchronous JavaScript allows code execution to continue while waiting for time-consuming operations, such as fetching data from an API or reading files, to complete.
3.JavaScript Concurrency Models
- This prevents blocking the main thread and enhances performance, especially in web applications.
- Callbacks: Functions passed as arguments to other functions to handle async tasks.
- Promises: Objects representing eventual completion or failure of an async operation.
- Async/Await: A modern way to write asynchronous code using promises in a synchronous-like manner.
2. Callbacks and Error Handling
1. Callbacks
What are Callbacks?
- Functions as First-Class Citizens: Functions can be assigned to variables, passed as arguments, and returned from other functions.
- Passing Functions as Arguments: Allows asynchronous code execution after an operation completes.
- A
callbackis a function passed as an argument to another function to be executed later.
function fetchData(callback) { setTimeout(() => { console.log("Data fetched!"); callback(); }, 2000); } function processData() { console.log("Processing data..."); } fetchData(processData); // data will be fetched after 2 second.- A
2. Callback Hell & Error Handling
Callback Hell and How to Avoid It
- Pyramid of Doom: Nested callbacks leading to unreadable code.
- Code Readability Issues: Deeply nested callbacks make debugging difficult.
Problem:Callback HellToo many nested functions.- (nested callbacks make code hard to read and maintain).
function getBread(callback) { setTimeout(() => { console.log("Got Bread 🍞"); // Output after 1 sec callback(); }, 1000); } function addButter(callback) { setTimeout(() => { console.log("Added Butter 🧈"); // Output after 2 sec callback(); }, 1000); } function addFilling(callback) { setTimeout(() => { console.log("Added Filling 🥪"); // Output after 3 sec callback(); }, 1000); } // Nested callbacks (callback hell) getBread(() => { addButter(() => { addFilling(() => { console.log("Sandwich is Ready!"); // Output after 4 sec }); }); }); /* Output: Got Bread 🍞 (after 1 sec) Added Butter 🧈 (after 2 sec) Added Filling 🥪 (after 3 sec) Sandwich is Ready! 🎉 (after 4 sec) */
- Error Handling in Callbacks
callback(error, result): Standard convention to handle errors and results in callbacks.- The Node.js Error-First Callback Pattern:
- The first parameter of a callback function is an error object (
callback(error, result)).
3. Promises: A Better Way to Handle Async Code
1. What is Promises
What is a Promise?
A promise represents a value that may be available now, later, or never.- A Promise is an object representing the eventual completion or failure of an asynchronous operation.
- a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.
- States: Pending, Fulfilled, Rejected.
- Methods:
.then(),.catch(),.finally().
- A
Promiserepresents a value that will be available in the future (resolved) or an operation that fails (rejected). Advantages: Promises solve callback hell and provide better error handling. No deep nesting, easier to read.

2. Chaining Promises
Chaining Promises
Disadvantages: we have to do promise chaining everytime we want to use it.- Enables handling sequential async tasks without nesting.
When a .then() method returns a promise, the next .then() waits for it to resolve before executing.
function getBread() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Got Bread 🍞"); // Output after 1 sec
resolve();
}, 1000);
});
}
function addButter() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Added Butter 🧈"); // Output after 2 sec
resolve();
}, 1000);
});
}
function addFilling() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Added Filling 🥪"); // Output after 3 sec
resolve();
}, 1000);
});
}
// Promise chaining
getBread()
.then(() => addButter())
.then(() => addFilling())
.then(() => console.log("Sandwich is Ready!")); // Output after 4 sec
/*
Output:
Got Bread 🍞 (after 1 sec)
Added Butter 🧈 (after 2 sec)
Added Filling 🥪 (after 3 sec)
Sandwich is Ready! 🎉 (after 4 sec)
*/3. Error Handling in Promises
.catch()for Errors: Catches and handles errors in promise chains.- Propagating Errors: Ensures errors bubble up to the correct handler.
function getUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("User fetched");
resolve({ userId, name: "John Doe" });
}, 1000);
});
}
function getOrders(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Orders fetched for ${user.name}`);
resolve(["Order1", "Order2", "Order3"]);
}, 1000);
});
}
function getOrderDetails(order) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Details fetched for ${order}`);
resolve({ order, details: "Item details" });
}, 1000);
});
}
// Chaining promises
getUser(1)
.then(user => getOrders(user))
.then(orders => getOrderDetails(orders[0]))
.then(orderDetails => console.log("Final order details:", orderDetails))
.catch(error => console.error("Error:", error));Common Pitfalls with Promises
- Forgetting to Return a Promise: Leads to unexpected behaviors.
- Nested Promises: Reduces readability and maintainability.
4. Async/Await: Modern Asynchronous Code
1. Async/Await
What is Async/Await?
asyncfunctions: Functions that return promises implicitly.awaitkeyword: Pauses execution until a promise resolves.
// Function to simulate getting bread
function getBread() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Got Bread 🍞"); // Output after 1 sec
resolve();
}, 1000);
});
}
// Function to simulate adding butter
function addButter() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Added Butter 🧈"); // Output after 2 sec
resolve();
}, 1000);
});
}
// Function to simulate adding filling
function addFilling() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Added Filling 🥪"); // Output after 3 sec
resolve();
}, 1000);
});
}
// Async function to make the sandwich
async function makeSandwich() {
await getBread(); // Waits 1 second
await addButter(); // Waits 1 more second
await addFilling(); // Waits 1 more second
console.log("Sandwich is Ready! 🎉"); // Output after 4 sec
}
// Call the function
makeSandwich();
/*
Expected Output:
Got Bread 🍞 (after 1 sec)
Added Butter 🧈 (after 2 sec)
Added Filling 🥪 (after 3 sec)
Sandwich is Ready! 🎉 (after 4 sec)
*/2. Error Handling in Async/Await
Error Handling in Async/Await
- Using
try...catch: Gracefully handles errors in async functions. - Combining with
.catch(): Alternative error handling approach.
- Using
// Function to simulate getting bread
function getBread() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Got Bread 🍞"); // Output after 1 sec
resolve();
}, 1000);
});
}
// Function to simulate adding butter
function addButter() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Added Butter 🧈"); // Output after 2 sec
resolve();
}, 1000);
});
}
// Function to simulate adding filling
function addFilling() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Added Filling 🥪"); // Output after 3 sec
resolve();
}, 1000);
});
}
// Async function to make the sandwich with error handling
async function makeSandwich() {
try {
await getBread(); // Waits 1 second
await addButter(); // Waits 1 more second
await addFilling(); // Waits 1 more second
console.log("Sandwich is Ready! 🎉"); // Output after 4 sec
} catch (error) {
console.error("An error occurred while making the sandwich:", error);
}
}
// Call the function
makeSandwich();Parallel Execution with Async/Await
Promise.all(): Runs multiple promises in parallel.Promise.race(),Promise.any(),Promise.allSettled(): Different strategies for handling multiple promises.
Best Practices for Async/Await
- Handling Multiple API Calls: Optimize execution using
Promise.all(). - Performance Optimization: Avoid blocking execution.
- Handling Multiple API Calls: Optimize execution using
Async/Await (Modern Approach)
Async/Awaitmakes asynchronous code looksynchronous, improving readability.- async makes a function return a
promise. - await pauses execution until the promise resolves.
- Reduces complexity compared to .then() chaining.
- async makes a function return a
5. Networking in JavaScript: Fetch & Axios
Using
fetch()API- Making GET, POST, PUT, DELETE Requests.
- Handling JSON Responses.
- Handling Errors in
fetch().
Using Axios for API Calls
- Why Axios? Provides simpler syntax and automatic JSON parsing.
- GET, POST, PUT, DELETE Requests.
- Axios Interceptors: Modify requests and responses globally.
AbortController: Cancelling Requests
- Using
AbortControllerwith Fetch to terminate API requests. - Cancelling API Calls with Axios using request cancellation tokens.
- Using
6. REST APIs
What is REST?
- HTTP Methods: GET, POST, PUT, DELETE define API interactions.
- Status Codes: Indicate response status (200, 400, 404, 500, etc.).
Working with REST APIs
- Authentication: API Keys, OAuth, JWT for secure access.
- Rate Limiting: Restricts API requests to prevent abuse.
- CORS (Cross-Origin Resource Sharing): Security policy restricting resource sharing between origins.
Working with Open APIs (Public APIs)
- Fetching Data from Public APIs.
- Pagination and Query Parameters for handling large datasets.
7. WebSockets & Real-Time Communication
Understanding WebSockets
- Difference Between HTTP & WebSockets: WebSockets provide full-duplex communication.
- WebSocket Lifecycle:
open,message,close,errorevents.
Using WebSockets in JavaScript
- Creating a WebSocket Connection.
- Sending & Receiving Messages.
Using Socket.IO for Real-Time Apps
- Server & Client Setup.
- Broadcasting Messages efficiently.
8. Advanced Networking Concepts
GraphQL APIs
- REST vs GraphQL: GraphQL allows flexible queries.
- Querying Data with GraphQL.
- Fetching Data using Apollo Client.
Streaming APIs
- Server-Sent Events (SSE): Push updates from the server.
- WebSockets for Streaming Data.
Rate Limiting & Throttling API Requests
debounce()&throttle()prevent excessive API calls.- Using API Rate Limiters.
Handling Large Data Responses Efficiently
- Pagination splits large data sets into smaller chunks.
- Infinite Scroll loads data dynamically as the user scrolls.
9. Performance Optimization in API Calls
Caching API Responses
- HTTP Caching, LocalStorage, SessionStorage, Service Workers.
Reducing API Calls
- Batch API Requests, Debouncing & Throttling.
Optimizing Fetch with Streams
ReadableStreamfor processing large responses incrementally.
10. Security Considerations in API Calls
Protecting API Keys
- Using Environment Variables to store API keys securely.
- Avoiding Hardcoding API Keys in code.
Handling CORS Issues
- Understanding and resolving CORS policy restrictions.
Cross-Site Scripting (XSS) & CSRF Protection
- Preventing XSS: Use input validation and escaping techniques.
- Implementing CSRF Tokens: Use anti-CSRF tokens to prevent unauthorized form submissions.