TypeDrop
2026-07-13 Challenge
2026-07-13
Medium
Typed Retry-with-Backoff Fetcher
You're building the resilient HTTP layer for a microservice that calls third-party APIs known to occasionally return transient errors. Each request must be retried with exponential backoff, errors must be classified as retryable or fatal, and every outcome — success or exhausted retries — must be surfaced as a fully-typed `FetchResult<T>` with zero `any`.
Goals
- Implement `classifyError` to categorize HTTP status codes and network failures into `retryable` or `fatal` kinds.
- Implement `computeDelay` to calculate exponential backoff delays capped by an optional `maxDelayMs`.
- Implement `fetchWithRetry<T>` to drive the retry loop: short-circuit on fatal errors, exhaust retries on persistent retryable errors, and validate the body on success.
- Implement the three type-guard functions (`isSuccess`, `isExhausted`, `isFatal`) that narrow a `FetchResult<T>` to its specific variant.
challenge.ts
// Key types at a glance:
export type ErrorKind = "retryable" | "fatal";
export interface ClassifiedError {
kind: ErrorKind;
status?: number;
message: string;
}
export type FetchResult<T> =
| { outcome: "success"; data: T; attempts: number }
| { outcome: "exhausted"; lastError: ClassifiedError; attempts: number }
| { outcome: "fatal"; error: ClassifiedError; attempts: number };
export type FetchAdapter =
(url: string) => Promise<{ status: number; body: unknown }>;
export type BodyValidator<T> = (body: unknown) => T;
// Your main function to implement:
export async function fetchWithRetry<T>(
url: string,
adapter: FetchAdapter,
validate: BodyValidator<T>,
config: RetryConfig
): Promise<FetchResult<T>> { /* TODO */ }
Hints (click to reveal)
Hints
- The discriminant field is `outcome` — use it in your type guards with a simple `result.outcome === 'success'` check.
- In `fetchWithRetry`, wrap the `adapter(url)` call in a `try/catch`: a thrown error means a network failure (no status), while a resolved value with a bad status is a classified HTTP error.
- For `computeDelay`, `Math.min(x, Infinity)` is always `x` — so you can use `config.maxDelayMs ?? Infinity` without an extra branch.
Useful resources
Or clone locally
git clone -b challenge/2026-07-13 https://github.com/niltonheck/typedrop.git