pending
This is the starting state, meaning the task is still running.
Asynchronous JavaScript lets your app keep running while it waits for slow tasks, like API calls, file operations, or timers. Without async patterns like this, the UI would freeze, and users would have to wait for every single task to finish before they could do anything else.
console.log("A");console.log("B");console.log("C");Here, each line waits for the line before it to finish first.
console.log("A");setTimeout(() => console.log("B"), 0);console.log("C");The output here is A, then C, then B, because the timeout callback only runs later, after the rest of the normal code has already finished.
A callback is simply a function that you pass into another function, which gets run later on.
function getData(callback) { setTimeout(() => { callback("Data loaded"); }, 1000);}
getData((result) => { console.log(result);});Callbacks are simple and still useful, but when you nest a lot of them inside each other, the code becomes hard to read.
Callback hell happens when many async steps depend on each other, one after another, and end up creating a lot of deep nesting.
loginUser((user) => { getProfile(user.id, (profile) => { getOrders(profile.id, (orders) => { getInvoice(orders[0].id, (invoice) => { console.log(invoice); }); }); });});A Promise stands for a value that will be available now, later, or maybe never at all. A promise can be in three possible states:
pending
This is the starting state, meaning the task is still running.
fulfilled
This means the task finished successfully.
rejected
This means the task failed.
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 the task as successful and passes the result forward. reject marks the task as failed and passes an error forward instead.
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 lets you write your async steps one after another in a straight line, instead of nesting them deeply inside each other like with callbacks.
async and await are special syntax built on top of promises. They let your async code look much closer to normal, 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);}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 to use, since it is built around promises. XHR is older, but you will still come across it in some older (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; }}async/await to keep complicated flows easy to read.try/catch.Content-Type and your auth tokens.