Worked review 1: a fetch-and-render service
This is the first of four full reviews. Each one gives you a complete, AI-style program, asks you to find the planted bugs using the last four lessons’ habits, then shows the real tools’ real output and a fixed version that actually compiles and runs correctly. This one is a small service that fetches a user record and renders a welcome message — a boundary, a floating promise, a non-null assertion, and a default value, all in about 35 lines.
The program
Section titled “The program”Review this before reading further
// @runinterface User { id: number; name: string; nickname?: string }
async function fetchUserJson(id: number): Promise<string> { // stands in for a real fetch(`/api/users/${id}`).then(r => r.text()) return id === 1 ? '{"id": 1, "name": "Ada"}' : '{"id": 2, "name": "Grace", "nickname": "Amazing Grace"}';}
async function fetchUser(id: number): Promise<User> { const raw = await fetchUserJson(id); return JSON.parse(raw) as User; // BUG 1: as -- shape never validated}
async function logAccessAsync(id: number): Promise<void> { await new Promise<void>((resolve) => setTimeout(resolve, 5)); console.log(`access logged for user ${id}`);}
async function renderProfile(id: number): Promise<string> { logAccessAsync(id); // BUG 2: floating promise -- missing await const user = await fetchUser(id); const displayName = user.nickname!.toUpperCase(); // BUG 3: non-null assertion on optional field return `Welcome, ${displayName}`;}
interface RenderOptions { retries?: number }
function getRetryCount(options: RenderOptions): number { return options.retries || 3; // BUG 4: || instead of ?? -- explicit 0 silently becomes 3}
async function main(): Promise<void> { console.log("retries for explicit 0:", getRetryCount({ retries: 0 })); console.log(await renderProfile(2)); console.log(await renderProfile(1)); // user 1 has no nickname -- crashes}
main(); Click Check types: tsc --strict reports zero errors. Click Run: it prints two lines, then crashes.
Real output:
retries for explicit 0: 3Welcome, AMAZING GRACETypeError: Cannot read properties of undefined (reading 'toUpperCase') at renderProfile (...)Notice what is missing from that output entirely: access logged for user 2 never prints. logAccessAsync(2)’s 5ms timer never gets the chance to fire, because the process crashes first — the exact danger a floating promise creates: its side effect can simply vanish, with nothing to show it was ever supposed to happen.
Find the bugs before reading on
Section titled “Find the bugs before reading on”Four planted bugs, one boundary, one async, two nullability. Try to name all four — with a line number and what actually goes wrong — before opening the answers below.
Bug 1 — line 12, JSON.parse(raw) as User
Symptom: fetchUser compiles as if it always returns a real User, but nothing checks that the parsed JSON actually has an id and a name.
Evidence: it happens to work for both ids in this program only because both fake payloads are well-formed — the moment the boundary function returns something else (a 404 HTML page, an error object, a partial record from a flaky backend), as User still compiles and still hands the caller a value it will treat as trustworthy.
Fix: a real type guard, checked against the parsed value before it is trusted:
interface User { id: number; name: string; nickname?: string }
function isUser(value: unknown): value is User { if (typeof value !== "object" || value === null) return false; const v = value as Record<string, unknown>; return typeof v.id === "number" && typeof v.name === "string";}Bug 2 — line 21, logAccessAsync(id); with no await
Symptom: the access-log side effect is fired and immediately forgotten; renderProfile moves on to the next line without waiting for it.
Evidence: the real output above proves it directly — access logged for user 2 never printed at all, because the process crashed on the very next call to renderProfile before the 5ms timer had a chance to fire. In a longer-running server this would just as easily lose the log silently with no crash at all, just an unlucky ordering.
Fix: await it.
// @skip-verify partial snippet, illustrates the fix only -- see the full fixed version belowasync function renderProfile(id: number): Promise<string> { await logAccessAsync(id); // ...}Bug 3 — line 23, user.nickname!.toUpperCase()
Symptom: nickname is typed string | undefined for a reason — not every user has one — and ! tells the checker to ignore that entirely.
Evidence: the real crash. User 1’s payload has no nickname; user.nickname! is undefined at runtime, and .toUpperCase() throws TypeError: Cannot read properties of undefined (reading 'toUpperCase').
Fix: fall back to something that is always present, like name:
// @skip-verify partial snippet, illustrates the fix only -- see the full fixed version belowconst displayName = (user.nickname ?? user.name).toUpperCase();Bug 4 — line 30, options.retries || 3
Symptom: a caller who explicitly passes retries: 0 (meaning “do not retry at all”) silently gets 3 instead.
Evidence: the first line of real output above — retries for explicit 0: 3, not 0.
Fix: ?? only falls through on null/undefined, not on every falsy value:
// @skip-verify partial snippet, illustrates the fix only -- see the full fixed version belowfunction getRetryCount(options: RenderOptions): number { return options.retries ?? 3;}The fixed version
Section titled “The fixed version”// @runinterface User { id: number; name: string; nickname?: string }
function isUser(value: unknown): value is User { if (typeof value !== "object" || value === null) return false; const v = value as Record<string, unknown>; return typeof v.id === "number" && typeof v.name === "string";}
async function fetchUserJson(id: number): Promise<string> { return id === 1 ? '{"id": 1, "name": "Ada"}' : '{"id": 2, "name": "Grace", "nickname": "Amazing Grace"}';}
async function fetchUser(id: number): Promise<User> { const raw = await fetchUserJson(id); const parsed: unknown = JSON.parse(raw); if (!isUser(parsed)) throw new Error(`invalid user payload for id ${id}`); return parsed;}
async function logAccessAsync(id: number): Promise<void> { await new Promise<void>((resolve) => setTimeout(resolve, 5)); console.log(`access logged for user ${id}`);}
async function renderProfile(id: number): Promise<string> { await logAccessAsync(id); const user = await fetchUser(id); const displayName = (user.nickname ?? user.name).toUpperCase(); return `Welcome, ${displayName}`;}
interface RenderOptions { retries?: number }
function getRetryCount(options: RenderOptions): number { return options.retries ?? 3;}
async function main(): Promise<void> { console.log("retries for explicit 0:", getRetryCount({ retries: 0 })); console.log(await renderProfile(2)); console.log(await renderProfile(1));}
main();Real output:
retries for explicit 0: 0access logged for user 2Welcome, AMAZING GRACEaccess logged for user 1Welcome, ADAEvery log line now appears, in the right order, and user 1 — who has no nickname — falls back to their name instead of crashing.
What the tools caught
Section titled “What the tools caught”tsc --strict on the buggy version: zero errors, on both the buggy and fixed versions — this entire review is invisible to the compiler alone.
typescript-eslint’s typed rules, run on the buggy version:
21:3 error Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator @typescript-eslint/no-floating-promises23:23 error Forbidden non-null assertion @typescript-eslint/no-non-null-assertion30:26 error Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator @typescript-eslint/prefer-nullish-coalescing
✖ 3 problems (3 errors, 0 warnings)Three of the four bugs, caught directly and specifically. Bug 1 — the as User boundary cast — is the one no tool in this course’s ladder catches; it took actually reading the boundary, and running the code, to find it.