Async JavaScript in Node.js
Async JavaScript in Node.js
Almost everything interesting in Node.js is asynchronous — API calls, database queries, file reads, network requests. Understanding async code is one of the most important skills for working with AI-generated backends.
Why Async Matters
When your server calls an AI API, that call might take 2-5 seconds. Without async, your server would freeze and couldn't handle any other requests during that time.
Synchronous (blocking):
Request 1 starts -> waits 3 seconds for API -> responds
Request 2 starts -> waits 3 seconds -> responds
Total time: 6 seconds
Asynchronous (non-blocking):
Request 1 starts -> sends API call -> (keeps working)
Request 2 starts -> sends API call -> (keeps working)
Request 1 API returns -> responds
Request 2 API returns -> responds
Total time: ~3 seconds
Node.js handles this through three patterns. Let's look at each one.
Pattern 1: Callbacks (The Old Way)
Callbacks are functions you pass to other functions, which get called when the work is done:
import fs from "fs"; // The callback pattern: function(error, result) fs.readFile("data.txt", "utf-8", (err, content) => { if (err) { console.error("Error:", err.message); return; } console.log("File content:", content); }); console.log("This runs BEFORE the file is read!");
Output:
This runs BEFORE the file is read!
File content: (contents of data.txt)
The problem with callbacks? Callback hell — nested callbacks that become unreadable:
// Callback hell — don't do this! fs.readFile("config.json", "utf-8", (err, config) => { if (err) return handleError(err); db.connect(JSON.parse(config), (err, connection) => { if (err) return handleError(err); connection.query("SELECT * FROM users", (err, users) => { if (err) return handleError(err); // Finally we have our users, 3 levels deep... }); }); });
You'll rarely write callbacks in modern code, but you'll see them in older Node.js examples and libraries.
Pattern 2: Promises (The Better Way)
A Promise represents a value that will be available in the future. It's either:
- Pending — still working
- Fulfilled — done successfully (has a result)
- Rejected — failed (has an error)
// Creating a Promise function delay(ms) { return new Promise((resolve) => { setTimeout(() => resolve("Done!"), ms); }); } // Using a Promise with .then() and .catch() delay(2000) .then((result) => { console.log(result); // "Done!" (after 2 seconds) }) .catch((error) => { console.error("Error:", error); });
Chaining Promises
Promises can be chained — each .then() returns a new Promise:
fetch("https://api.example.com/users") .then((response) => response.json()) .then((users) => { console.log("Got users:", users.length); return users.filter((u) => u.isActive); }) .then((activeUsers) => { console.log("Active users:", activeUsers.length); }) .catch((error) => { console.error("Something went wrong:", error); });
This is cleaner than callbacks, but there's an even better way.
Pattern 3: async/await (The Modern Way)
async/await is syntactic sugar on top of Promises that makes async code look like normal, synchronous code. This is what you should use and what AI tools generate.
// Mark a function as async async function getUsers() { // 'await' pauses execution until the Promise resolves const response = await fetch("https://api.example.com/users"); const users = await response.json(); return users; } // Arrow function version const getUsers = async () => { const response = await fetch("https://api.example.com/users"); const users = await response.json(); return users; };
Compare the three styles side by side:
// Callbacks fs.readFile("data.json", "utf-8", (err, raw) => { if (err) return console.error(err); const data = JSON.parse(raw); console.log(data); }); // Promises (.then) fs.promises.readFile("data.json", "utf-8") .then((raw) => JSON.parse(raw)) .then((data) => console.log(data)) .catch((err) => console.error(err)); // async/await (cleanest!) async function loadData() { const raw = await fs.promises.readFile("data.json", "utf-8"); const data = JSON.parse(raw); console.log(data); }
What to ask your AI: "Convert this callback-based code to async/await: [paste code]"
Error Handling with try/catch
When using async/await, you handle errors with try/catch — just like synchronous code:
async function fetchUserData(userId: string) { try { const response = await fetch(`https://api.example.com/users/${userId}`); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const user = await response.json(); return user; } catch (error) { console.error("Failed to fetch user:", error.message); return null; // Return a fallback value } } // Usage const user = await fetchUserData("123"); if (user) { console.log("Got user:", user.name); } else { console.log("Could not load user"); }
Handling Errors in Express Routes
app.get("/api/users/:id", async (req, res) => { try { const user = await db.getUser(req.params.id); if (!user) { return res.status(404).json({ error: "User not found" }); } res.json(user); } catch (error) { console.error("Database error:", error); res.status(500).json({ error: "Internal server error" }); } });
Promise.all for Parallel Operations
When you need to do multiple async things that don't depend on each other, run them in parallel with Promise.all:
// Sequential — slow (each waits for the previous one) const users = await fetchUsers(); // 1 second const products = await fetchProducts(); // 1 second const orders = await fetchOrders(); // 1 second // Total: ~3 seconds // Parallel — fast (all run at the same time) const [users, products, orders] = await Promise.all([ fetchUsers(), // 1 second fetchProducts(), // 1 second (starts at the same time) fetchOrders(), // 1 second (starts at the same time) ]); // Total: ~1 second
Other Promise Utilities
// Promise.allSettled — wait for all, even if some fail const results = await Promise.allSettled([ fetchUsers(), fetchProducts(), fetchOrders(), ]); results.forEach((result) => { if (result.status === "fulfilled") { console.log("Success:", result.value); } else { console.log("Failed:", result.reason); } }); // Promise.race — return whichever finishes first const fastest = await Promise.race([ fetchFromServer1(), fetchFromServer2(), ]);
What to ask your AI: "I'm making three independent API calls. Convert them to run in parallel with Promise.all."
Common Async Patterns in AI Apps
Calling an AI API
import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); async function askClaude(question: string): Promise<string> { try { const response = await client.messages.create({ model: "claude-sonnet-4-20250514", max_tokens: 1024, messages: [{ role: "user", content: question }], }); return response.content[0].text; } catch (error) { console.error("AI API error:", error); throw error; } }
Database Query
async function getUserWithPosts(userId: string) { const [user, posts] = await Promise.all([ db.collection("users").doc(userId).get(), db.collection("posts").where("authorId", "==", userId).get(), ]); return { ...user.data(), posts: posts.docs.map((doc) => doc.data()), }; }
Retry Pattern
async function fetchWithRetry(url: string, maxRetries = 3): Promise<any> { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { console.log(`Attempt ${attempt}/${maxRetries} failed: ${error.message}`); if (attempt === maxRetries) throw error; // Wait before retrying (exponential backoff) await new Promise((r) => setTimeout(r, 1000 * attempt)); } } }
Common Mistakes
| Mistake | Fix |
|---|---|
Forgetting await | Your variable will be a Promise instead of the actual value |
Using await outside an async function | Wrap your code in an async function |
| Not handling errors | Always use try/catch with await |
| Sequential when parallel is possible | Use Promise.all for independent operations |
Forgetting .json() on fetch | fetch returns a Response — call .json() to get the data |
What's Next?
You now understand async programming — one of the most important concepts in Node.js. The final tutorial is your Node.js Cheat Sheet — a quick reference for all the concepts and patterns covered in this book, plus AI prompts you can use right away.
What to ask your AI: "I'm calling three APIs sequentially. Can you refactor this to use Promise.all and add proper error handling?"