Skip to content

Asynchronous JavaScript

Asynchronous JavaScript lets your app continue running while waiting for slow tasks like API calls, file operations, or timers. Without async patterns, the UI can freeze and users must wait for every task to finish before doing anything else.


console.log("A");
console.log("B");
console.log("C");

Each line waits for the previous line.

graph TD A[Start] --> B[Run Sync Code] B --> C[Async Task Sent to Web APIs] C --> D[Callback Queue] D --> E[Event Loop Moves Task to Call Stack] E --> F[Callback Executes]

A callback is a function passed into another function and run later.

function getData(callback) {
setTimeout(() => {
callback("Data loaded");
}, 1000);
}
getData((result) => {
console.log(result);
});

Callbacks are simple and still useful, but deeply nested callbacks become hard to read.


Callback hell happens when many async steps depend on each other and create deep nesting.

loginUser((user) => {
getProfile(user.id, (profile) => {
getOrders(profile.id, (orders) => {
getInvoice(orders[0].id, (invoice) => {
console.log(invoice);
});
});
});
});
graph TD A[loginUser] --> B[getProfile] B --> C[getOrders] C --> D[getInvoice] D --> E[Deep Nesting and Hard Debugging]

A Promise represents a value that will be available now, later, or never. A promise has three states:

pending

Initial state. Operation still running.

fulfilled

Operation completed successfully.

rejected

Operation failed.

graph LR A[pending] -->|resolve| B[fulfilled] A -->|reject| C[rejected]
const wait = (ms) => {
return new Promise((resolve, reject) => {
if (typeof ms !== "number") {
reject(new Error("ms must be a number"));
return;
}
setTimeout(() => {
resolve(`Done in ${ms}ms`);
}, ms);
});
};

resolve marks success and passes data forward. reject marks failure and passes an error.

wait(500)
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error.message);
})
.finally(() => {
console.log("Always runs");
});

getUser()
.then((user) => getOrders(user.id))
.then((orders) => getInvoice(orders[0].id))
.then((invoice) => console.log(invoice))
.catch((error) => console.error("Flow failed:", error));

Chaining makes async steps linear and avoids deep callback nesting.


async and await are syntax built on promises. They make async code look close to synchronous code.

async function loadDashboard() {
try {
const user = await getUser();
const orders = await getOrders(user.id);
console.log(orders);
} catch (error) {
console.error("Could not load dashboard", error);
}
}

async function createPost() {
const response = await fetch("https://example.com/api/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer your-token"
},
body: JSON.stringify({
title: "Hello",
content: "Learning async deeply"
})
});
const responseHeaders = response.headers;
const contentType = responseHeaders.get("content-type");
const data = await response.json();
console.log("Response content-type:", contentType);
console.log("Response body:", data);
}

Reading Request Headers and Body on Server

Section titled “Reading Request Headers and Body on Server”
app.post("/api/posts", express.json(), (req, res) => {
console.log("Auth header:", req.headers.authorization);
console.log("Request body:", req.body);
res.setHeader("x-app-version", "1.0.0");
res.json({ ok: true, received: req.body });
});

const xhr = new XMLHttpRequest();
xhr.open("POST", "https://example.com/api/posts");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", "Bearer your-token");
xhr.onload = function () {
console.log("Status:", xhr.status);
console.log("Response headers:\n", xhr.getAllResponseHeaders());
console.log("Response body:", xhr.responseText);
};
xhr.onerror = function () {
console.error("Network error");
};
xhr.send(JSON.stringify({ title: "Hello", content: "Sent via XHR" }));

fetch is usually cleaner and promise-based. XHR is older but still appears in legacy code.


try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Request failed:", error.message);
}
fetch("/api/data")
.then((res) => {
if (!res.ok) throw new Error("Bad response");
return res.json();
})
.catch((error) => {
console.error(error.message);
});
class ApiError extends Error {
constructor(message, status) {
super(message);
this.status = status;
}
}

graph TD A[Start Request] --> B[Send Headers and Body] B --> C[Server Processes] C --> D[Receive Response Headers] D --> E[Parse Response Body] E --> F{Success?} F -->|Yes| G[Update UI] F -->|No| H[Handle Error]

  1. Use async/await for readability in complex flows.
  2. Keep one try/catch around each logical async unit.
  3. Validate response status before parsing body.
  4. Send clear headers (Content-Type, auth tokens).
  5. Show user-friendly error messages, not raw stack traces.