TypeScript Fundamentals: Types, Generics and Real-World Patterns · Generics, Utility Types and Real-World TypeScript
Async Code and Typed Error Handling
Type promises and async functions, run tasks in parallel with Promise.all, handle thrown values safely as unknown, and model failures explicitly with a Result type.
What you'll build
A mock user service: async functions that fetch data, parallel loading with Promise.all, a custom error class, and a Result wrapper so callers cannot forget to handle failure.
The idea
- Promises are generic.
Promise<User>is a promise that resolves to aUser. Anasyncfunction always returns a promise, soasync function load(): Promise<User>. awaitunwraps the promise.Awaited<T>is the type-level equivalent.Promise.allkeeps types positional:Promise.all([getUser(), getPosts()])returns a tuple[User, Post[]].- Errors are
unknown. Incatch (error), TypeScript (withuseUnknownInCatchVariables, part of strict) typeserrorasunknown, because anything can be thrown. Narrow withinstanceof Error. - Custom errors extend
Errorand carry data (a code, a status). Narrow them withinstanceof. - A
Resulttype makes failure part of the return type. TypeScript doesn't track which exceptions a function throws, so for expected failures (validation, "not found") returning{ ok: false, error }forces the caller to deal with it.
Use exceptions for unexpected problems and Result for expected ones.
The code
interface User {
id: number;
name: string;
}
class NotFoundError extends Error {
constructor(public readonly id: number) {
super(`user ${id} not found`);
this.name = "NotFoundError";
}
}
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
const database: User[] = [
{ id: 1, name: "Ada" },
{ id: 2, name: "Grace" },
];
async function getUser(id: number): Promise<User> {
await delay(5);
const user = database.find((u) => u.id === id);
if (!user) throw new NotFoundError(id);
return user;
}
async function getPostCount(id: number): Promise<number> {
await delay(5);
return id * 3;
}
// Parallel work with a typed tuple result
const [user, posts] = await Promise.all([getUser(1), getPostCount(1)]);
console.log(`${user.name} has ${posts} posts`);
// Errors are unknown: narrow before using them
try {
await getUser(99);
} catch (error) {
if (error instanceof NotFoundError) {
console.log("caught NotFoundError for id", error.id);
} else if (error instanceof Error) {
console.log("other error:", error.message);
}
}
// Turning an exception into a Result so callers must handle both cases
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
async function tryGetUser(id: number): Promise<Result<User>> {
try {
return { ok: true, value: await getUser(id) };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) };
}
}
for (const id of [2, 7]) {
const result = await tryGetUser(id);
console.log(result.ok ? `found ${result.value.name}` : `failed: ${result.error}`);
}
// Promise.allSettled keeps going when some promises fail
const settled = await Promise.allSettled([getUser(1), getUser(3)]);
console.log(settled.map((s) => s.status));
Output
Ada has 3 posts caught NotFoundError for id 99 found Grace failed: user 7 not found [ 'fulfilled', 'rejected' ]
Common mistakes
- Forgetting
await. The variable becomes aPromise<T>, not aT; the compiler usually warns when you then use it as aT. - Awaiting in a loop when the calls are independent. Start them together and
await Promise.all(...). - Assuming
catch (e)gives anError. Narrow withinstanceof Erroror check the shape before reading.message. - Throwing strings or plain objects. Throw
Errorsubclasses so you get stack traces andinstanceofchecks. - Ignoring rejected promises. Unhandled rejections crash Node.js processes; always
awaitor.catchthem.
Key points
asyncfunctions returnPromise<T>;awaitunwraps it, andPromise.allreturns a typed tuple.- Thrown values are
unknown; narrow withinstanceofbefore using them. - Custom
Errorsubclasses carry data and enable precisecatchbranches. - A
Resultdiscriminated union makes expected failures explicit in the type.
Further reading: The Handbook: Narrowing and MDN: async function.
Practice questions
Why is the variable in `catch (error)` typed `unknown` and how should you use it?
Answer
JavaScript allows throwing any value, so TypeScript cannot know its type. Narrow it first, for example with error instanceof Error before reading error.message, or with a custom error class check.
When would you return a Result type instead of throwing?
Answer
For expected, recoverable failures such as validation or a missing record. A Result forces the caller to handle both cases at compile time, while exceptions are better for unexpected faults.
Open in the app to mark lessons complete, take notes and track your progress.