Chapter 34: TypeScript Async Programming
Asynchronous programming in TypeScript very calmly, step-by-step, like we’re sitting together with VS Code open, a cup of chai on the table, and we’re writing small realistic examples one by one until everything feels natural and clear.
Async programming in TypeScript is almost identical to modern JavaScript — but TypeScript adds very strong type safety around promises, async/await, error handling, and especially around what gets returned from async functions.
In 2026 almost every serious backend (Node.js), frontend (React/Next.js), and full-stack application uses async code heavily — fetching data, reading files, calling APIs, timers, streams, WebSockets, etc.
1. The foundation: Promises (still very important in 2026)
Even though async/await is the most common syntax today, understanding promises deeply is still crucial — many libraries return promises, many errors happen because people don’t handle promise rejections properly.
Basic typed promise:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
function fetchUser(id: number): Promise<{ id: number; name: string }> { return fetch(`https://jsonplaceholder.typicode.com/users/${id}`) .then(response => { if (!response.ok) throw new Error("Network error"); return response.json(); }); } |
|
0 1 2 3 4 5 6 7 8 |
fetchUser(1) .then(user => console.log(user.name)) // string .catch(err => console.error(err.message)); // Error |
Modern style (2026) — almost everyone uses async/await instead of .then/.catch chains
2. async / await — the syntax most people write every day
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
async function getUserName(id: number): Promise<string> { try { const user = await fetchUser(id); // await → unwraps the Promise return user.name; } catch (err) { // err is unknown by default — very important safety feature if (err instanceof Error) { console.error(err.message); } return "Unknown user"; } } |
Key points TypeScript enforces / helps with:
- async functionalways returns a Promise (even if you return a plain value)
|
0 1 2 3 4 5 6 7 8 |
async function sayHello(): Promise<string> { return "Hello"; // automatically wrapped in Promise.resolve("Hello") } |
- await can only be used inside async functions (or top-level await in modules — more later)
- TypeScript infers the awaited type automatically
|
0 1 2 3 4 5 6 7 |
const user = await fetchUser(1); // user → { id: number; name: string } |
3. Error handling — one of the biggest differences from plain JS
In plain JavaScript → many people forget to catch → unhandled promise rejections crash Node.js or get swallowed silently.
In TypeScript (with strict mode) → much harder to ignore errors:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Very common safe pattern in 2026 async function safeFetch<T>(url: string): Promise<T | null> { try { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP {res.status}`); return await res.json() as T; } catch { return null; // or throw custom error, log, etc. } } |
Modern 2026 pattern — typed error + success union (very popular)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; async function fetchWithResult<T>(url: string): Promise<Result<T>> { try { const res = await fetch(url); if (!res.ok) throw new Error(`Status {res.status}`); const data = await res.json() as T; return { success: true, data }; } catch (err) { return { success: false, error: err instanceof Error ? err : new Error("Unknown error") }; } } // Usage — very clean narrowing const result = await fetchWithResult<User>("/api/user/1"); if (result.success) { console.log(result.data.name); // string } else { console.log(result.error.message); // string } |
4. Top-level await (very common in 2026 — especially in modules & scripts)
Since Node.js 14+ / browsers support it, and TypeScript 3.8+:
|
0 1 2 3 4 5 6 7 8 9 |
// main.ts (ESM module) const data = await fetch("https://api.example.com/data").then(r => r.json()); console.log(data); |
tsconfig.json setting needed (2026 default in many setups):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
{ "compilerOptions": { "module": "ESNext", "target": "ES2022", "moduleResolution": "bundler" // or "NodeNext" } } |
Very useful in:
- scripts / CLI tools
- Next.js API routes (before App Router)
- Vite / esbuild entry points
- server startup files
5. Typing async functions & Promises — common patterns
| Return type you want | Write this in function signature | What you can return inside |
|---|---|---|
| Promise<string> | async function fn(): Promise<string> | return “hello” or return Promise.resolve(“hello”) |
| Just the inner type | async function fn(): Promise<string> (most common) | return “hello” |
| void (fire-and-forget) | async function logEvent(): Promise<void> | return; or nothing |
| Union success/error | Promise<Result<User>> | see Result pattern above |
| Never throw (safe API) | Promise<T |
null>orPromise<Result<T>> |
6. Realistic full example — modern 2026 API service
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
interface User { id: number; name: string; email: string; } type ApiResult<T> = | { success: true; data: T } | { success: false; error: string }; class UserApi { private baseUrl = "https://jsonplaceholder.typicode.com"; async getUser(id: number): Promise<ApiResult<User>> { try { const res = await fetch(`${this.baseUrl}/users/${id}`); if (!res.ok) { throw new Error(`HTTP {res.status}`); } const data = await res.json() as User; return { success: true, data }; } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; return { success: false, error: message }; } } async getAllUsers(): Promise<ApiResult<User[]>> { try { const res = await fetch(`${this.baseUrl}/users`); if (!res.ok) throw new Error("Failed to fetch users"); const users = await res.json() as User[]; return { success: true, data: users }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : "Network error" }; } } } // Usage in React / Node / anywhere async function displayUser(id: number) { const result = await new UserApi().getUser(id); if (result.success) { console.log(`Hello {result.data.name}`); } else { console.error("Could not load user:", result.error); } } |
7. Quick cheat-sheet — async patterns 2026
| Pattern | Return type | Best when… |
|---|---|---|
| Simple fetch | Promise<T> | You let caller handle errors |
| Safe / never throw | Promise<T |
null>orPromise<Result<T>> |
| Throw custom error | Promise<T> + throw new AppError() | When you want centralized error handling |
| Top-level await | no explicit return needed | Entry files, scripts, tests |
| Parallel requests | Promise.all([…]) | Fetch multiple things at once |
| Sequential with early exit | await in loop + return on error | Validation chains, setup steps |
Want to go deeper into one area?
- Parallel vs sequential awaits with real examples
- Error boundaries & typed custom errors
- Typing streams / async iterators / generators
- How Next.js App Router / React Server Components handle async
- Common anti-patterns people still write in 2026
Just tell me which direction feels most useful right now 😄
