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.

Or clone locally

git clone -b challenge/2026-07-13 https://github.com/niltonheck/typedrop.git