JavaScript Async/Await

async/await Basics

Syntactic sugar over Promises
// async function always returns a Promise
async function fetchHost(name) {
    const response = await fetch(`/api/hosts/${name}`);
    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }
    const data = await response.json();
    return data;
}

// Arrow function variant
const fetchHost = async (name) => {
    const res = await fetch(`/api/hosts/${name}`);
    return res.json();
};

await pauses execution until the Promise resolves. The function returns a Promise to its caller. Rejected promises throw — use try/catch.

Error Handling

try/catch replaces .catch()
async function loadConfig(path) {
    try {
        const res = await fetch(path);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const config = await res.json();
        return config;
    } catch (err) {
        console.error("Config load failed:", err.message);
        return { port: 8080 };  // fallback defaults
    } finally {
        console.log("config loading complete");
    }
}

try/catch works naturally with await. The catch block handles both network errors and HTTP errors.

Parallel Execution

await Promise.all for concurrent operations
// Sequential — slow (each waits for the previous)
const hosts = await fetchHosts();
const ports = await fetchPorts();
const vlans = await fetchVlans();

// Parallel — fast (all start immediately)
const [hosts, ports, vlans] = await Promise.all([
    fetchHosts(),
    fetchPorts(),
    fetchVlans(),
]);

// Parallel with error tolerance
const results = await Promise.allSettled([
    fetchHosts(),
    fetchPorts(),
    fetchVlans(),
]);
const succeeded = results
    .filter(r => r.status === "fulfilled")
    .map(r => r.value);

Do not await each call sequentially when they are independent. Use Promise.all to run them concurrently.

Async Iteration

for-await-of — consume async streams
async function* generatePorts(start, end) {
    for (let port = start; port <= end; port++) {
        await checkPort(port);
        yield port;
    }
}

for await (const port of generatePorts(80, 443)) {
    console.log(`Port ${port} checked`);
}

// Reading a stream
const response = await fetch("/api/large-data");
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    console.log(decoder.decode(value));
}

Async generators (async function*) combine generators with async/await. for await…​of consumes them.

Common Patterns

Retry with exponential backoff
async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const res = await fetch(url);
            if (res.ok) return res.json();
            throw new Error(`HTTP ${res.status}`);
        } catch (err) {
            if (i === retries - 1) throw err;
            const delay = Math.pow(2, i) * 1000;
            console.log(`Retry ${i + 1} in ${delay}ms`);
            await new Promise(r => setTimeout(r, delay));
        }
    }
}
Timeout wrapper
function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error("timeout")), ms)
    );
    return Promise.race([promise, timeout]);
}

const data = await withTimeout(fetch("/api/slow"), 5000);

Promise.race with a timeout promise gives you a deadline for any async operation.

Top-Level Await

Available in ESM modules
// In an ES module (.mjs or type: "module" in package.json)
const config = await loadConfig();
const db = await connectDB(config.dbUrl);

export { db, config };

Top-level await blocks module loading until the promise resolves. Use it for initialization that must complete before the module is usable.