
Understanding Asynchronous Programming with Promises and async/await in JavaScript and TypeScript
Learn how JavaScript handles asynchronous operations with Promises and async/await — from callbacks to modern async flows, with real examples in both JS and TypeScript.
dogunfx
When you fetch data from an API, read a file, or wait for user input, your program can’t stop everything else from running. JavaScript solves this problem using asynchronous programming, allowing other code to keep running while waiting for slow tasks to finish.
This guide explores Promises and async/await, the modern tools for handling asynchronous code in JavaScript and TypeScript.
1) The Problem: Blocking vs Non-Blocking Code
In most programming languages, operations happen one after another:
// Blocking example
const data = readFileSync("data.txt"); // stops until done
console.log("Finished reading!");
In JavaScript, many APIs are non-blocking — they don’t stop the program.
// Non-blocking example
readFile("data.txt", (err, data) => {
console.log("Finished reading!");
});
console.log("This runs first!");
Output:
This runs first!
Finished reading!
This asynchronous nature is key to how JavaScript handles I/O.
2) Promises — The Foundation of Async
A Promise represents a value that will be available later.
Creating a Promise
const delay = (ms) =>
new Promise((resolve) => {
setTimeout(() => resolve(`Waited ${ms}ms`), ms);
});
delay(1000).then((msg) => console.log(msg));
Output (after 1s):
Waited 1000ms
Handling Errors
const riskyTask = new Promise((resolve, reject) => {
const ok = Math.random() > 0.5;
ok ? resolve("Success!") : reject("Failed!");
});
riskyTask
.then((msg) => console.log("✅", msg))
.catch((err) => console.error("❌", err))
.finally(() => console.log("Done."));
Output (randomly):
✅ Success!
Done.
or
❌ Failed!
Done.
3) Chaining Promises
Each .then() returns a new Promise, allowing chaining.
delay(500)
.then(() => delay(1000))
.then(() => delay(1500))
.then(() => console.log("All done!"));
Or shorter:
delay(500)
.then(() => delay(1000))
.then(console.log); // logs result of final delay
4) async/await — Cleaner Promise Syntax
async/await is syntactic sugar over Promises, making code look synchronous while remaining non-blocking.
async function run() {
console.log("Starting...");
const msg = await delay(1000);
console.log(msg);
console.log("Done!");
}
run();
Output:
Starting...
Waited 1000ms
Done!
Error Handling with try/catch
async function riskyRun() {
try {
const result = await riskyTask;
console.log("Result:", result);
} catch (err) {
console.error("Caught error:", err);
}
}
5) Running Multiple Async Tasks
In Sequence
await delay(1000);
await delay(2000);
console.log("3 seconds total");
In Parallel
await Promise.all([delay(1000), delay(2000)]);
console.log("Only 2 seconds total!");
With Promise.race()
const winner = await Promise.race([delay(1000), delay(2000)]);
console.log("Fastest:", winner);
6) Practical Example — Fetching Data
JavaScript
async function fetchTodo() {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
}
fetchTodo().catch(console.error);
TypeScript Version
interface Todo {
id: number;
title: string;
completed: boolean;
}
async function fetchTodo(): Promise<Todo> {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<Todo>;
}
fetchTodo()
.then((todo) => console.log(todo.title))
.catch(console.error);
7) Combining Async Logic
Example: Fetch Multiple APIs Together
async function fetchMultiple() {
const urls = [
"https://jsonplaceholder.typicode.com/todos/1",
"https://jsonplaceholder.typicode.com/users/1",
];
const [todoRes, userRes] = await Promise.all(urls.map((u) => fetch(u)));
const [todo, user] = await Promise.all([todoRes.json(), userRes.json()]);
console.log({ todo, user });
}
fetchMultiple();
Example: Retry Logic
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.warn(`Attempt ${i + 1} failed:`, err.message);
if (i === retries - 1) throw err;
}
}
}
8) Async Patterns in TypeScript
TypeScript makes async code safer with static typing.
async function getData(): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve("Done!"), 1000));
}
async function main(): Promise<void> {
const message = await getData();
console.log(message);
}
9) Common Mistakes
| Mistake | What Happens | Fix |
|---|---|---|
Forgetting await | Gets a Promise, not value | Use await myFunc() |
Using await in top-level JS (older Node) | SyntaxError | Wrap in async IIFE or enable top-level await |
Blocking loops (forEach) | Doesn’t await async calls | Use for...of instead |
| Forgetting error handling | Uncaught rejections | Wrap in try/catch or .catch() |
10) Cheat Sheet
| Feature | Example | Description |
|---|---|---|
| Create Promise | new Promise((res, rej) => {}) | Foundation of async ops |
| Resolve async | resolve(value) | Completes successfully |
| Reject async | reject(error) | Signals failure |
| Await Promise | await myPromise | Pauses until resolved |
| Handle error | try { await } catch {} | Safe async handling |
| Run parallel | Promise.all([...]) | Waits for all |
| Run fastest | Promise.race([...]) | Returns first result |
11) References
- MDN — Using Promises
- MDN — async/await
- TypeScript Handbook — Promises and Async/Await
- Node.js Docs — Event Loop
- Bun Docs — Async runtime
Asynchronous programming is what makes JavaScript feel fast — the engine doesn’t freeze while waiting for I/O. Once you master Promises and async/await, you’ll be ready to build APIs, work with databases, and handle real-time operations like a pro.