Skip to content
cccskills
FreePublished on GitHub

web-error-handling-result-types

TypeScript Result/Either types for type-safe error handling, railway-oriented programming patterns, error as values

The English summary is being prepared. Showing the original description.

See how to install

Files included (6)

  • SKILL.md11.8 KB
  • examples/async.md11.0 KB
  • examples/combining.md11.5 KB
  • examples/core.md11.3 KB
  • metadata.yaml370 B
  • reference.md4.2 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

TypeScript Result Type Patterns

Quick Guide: A Result<T, E> is a discriminated union on ok, so TypeScript refuses to read value until the caller has checked. That moves a function's failure modes into its signature, where an exception hides them. Use it for expected failures — validation, parsing, requests — and keep exceptions for bugs and for conditions nothing downstream can act on. A custom implementation is about forty lines and the recommended default; the whole surface is in this skill.

Detailed Resources:

  • examples/core.md — the Result module, typed error definitions, wrapping throwing code, pattern matching
  • examples/async.md — Promise<Result>, async chaining, retry, converting a promise
  • examples/combining.md — fail-fast, collect-all, object and sequential combination
  • reference.md — operation lookup, what Results do not catch, error-type templates

Which path applies

  • Nothing exists yet — write the module: the union, ok, err, map, flatMap, match, tryCatch. examples/core.md is the whole file.
  • A library owns the type — the operations are named differently but compose identically; reference.md maps the names.
  • The failing operation is async — the type is Promise<Result<T, E>> and the awaiting is the caller's; see examples/async.md.

<critical_requirements>

Before writing Result code

Check result.ok before reading value or error. The union narrows only through that check, so TypeScript will refuse either access until it is made — and a runtime undefined is what a bypassed check produces.

Wrap every throwing call inside a Result-returning function in tryCatch. JSON.parse and its kin throw past the return type, so one unwrapped call makes the signature a lie and the caller's exhaustive handling incomplete.

Give each error a discriminant field — code or type — rather than typing it as Error or string. The discriminant is what lets the caller switch and lets TypeScript check the switch is exhaustive; a bare message can only be displayed.

Chain with flatMap where each step returns a Result. The error type unions itself and the first failure short-circuits the rest, which is what nested if (result.ok) blocks are reimplementing by hand.

Do something with every Result you receive. A discarded one is a failure that never happened as far as the rest of the program is concerned, and no type error marks it.

</critical_requirements>


Auto-detection: Result type, Either type, ok err, railway-oriented programming, error as value, flatMap andThen, tryCatch, unwrapOr, combineWithAllErrors, discriminated union error, typed errors

Applies to:

  • Expected, recoverable failures — validation, parsing, requests, business rules
  • Function signatures that have to name every way they can fail
  • Chaining fallible steps so the first failure skips the rest
  • Collecting every failure at once, as form validation needs

Handled elsewhere:

  • Render-phase failures — a component that throws is caught by whatever wraps it, and a Result never reaches that path.
  • Transport and caching — a Result describes the outcome of a request; issuing, retrying and caching it belong to whatever fetches.
  • Schema validation — a validator that reports issues has its own result shape; wrap it at the boundary and carry its report as your error payload.
  • Turning a failure into a response — the status code an error maps to is the API layer's rule, and this skill only guarantees the error arrives typed.

<philosophy>

Philosophy

An exception is invisible control flow: it leaves no trace in the type, so the only way to know a function throws is to read it or to be surprised in production. A Result puts the same information in the signature, where the compiler enforces it.

The cost is real — every caller handles or propagates, and the error union grows as a chain lengthens. That is why the boundary matters: convert throwing code to Results on the way in, and convert Results to whatever the outside world wants on the way out. In between, nothing throws.

The railway: success runs the main line, and the first error switches to the parallel one, where every later step is skipped until something explicitly handles it.

     parseNumber     validatePositive     double
OK   ─────────────────────────────────────────────> success
                  ↘                  ↘
ERR                 ────────────────────────────> failure
</philosophy>

<decision_framework>

Result, exception, or nullable

Can the caller do something about this failure?
├─ NO — it is a bug or a condition nothing can act on → throw
│   ├─ Index out of bounds, invalid internal state
│   └─ Missing startup configuration, unreachable database at boot
└─ YES → What does the failure need to carry?
    ├─ Nothing but its own absence → T | null
    ├─ A reason the caller branches on → Result<T, E>
    └─ Several distinct reasons → Result<T, E> with a discriminated E

A Result<User, NotFoundError> whose error carries only code: "NOT_FOUND" is a nullable wearing a costume. Reach for the Result when the caller's next action differs by reason.

Fail fast or collect everything: one invalid field in a form is not a reason to hide the other four, so form validation collects; a chain where step two consumes step one's output has nothing to collect and short-circuits.

Returning a value also costs far less than throwing one, because a thrown error captures a stack trace and unwinds; reference.md carries the measured comparison. That is a tiebreaker on a hot path rather than a reason on its own.

</decision_framework>


<patterns>

Core patterns

Pattern 1: The type and its constructors

ok as the discriminant, readonly throughout, never on the other side so inference stays clean.

export type Result<T, E = Error> =
  | { readonly ok: true; readonly value: T }
  | { readonly ok: false; readonly error: E };

export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

Full code: examples/core.md

Pattern 2: map and mapError

Each transforms one side and passes the other through untouched, which is what makes them safe to apply to a Result you have not checked.

export const map = <T, U, E>(
  result: Result<T, E>,
  fn: (value: T) => U,
): Result<U, E> => (result.ok ? ok(fn(result.value)) : result);

export const mapError = <T, E, F>(
  result: Result<T, E>,
  fn: (error: E) => F,
): Result<T, F> => (result.ok ? result : err(fn(result.error)));

mapError is where context is added — the operation that failed, the input that caused it.

Pattern 3: flatMap for chaining

The step returns a Result of its own, so the error types union and the first failure ends the chain.

export const flatMap = <T, U, E, F>(
  result: Result<T, E>,
  fn: (value: T) => Result<U, F>,
): Result<U, E | F> => (result.ok ? fn(result.value) : result);

const parsed = flatMap(parseNumber(input), validatePositive);
// Result<number, ParseError | ValidationError>

Full code: examples/core.md

Pattern 4: tryCatch at the boundary

Throwing code is converted where it enters, and the error is mapped to this domain's type in the same call.

export const tryCatch = <T, E>(
  fn: () => T,
  onError: (error: unknown) => E,
): Result<T, E> => {
  try {
    return ok(fn());
  } catch (error) {
    return err(onError(error));
  }
};

const parsed = tryCatch(
  () => JSON.parse(json) as Config,
  (error): ParseError => ({
    code: "PARSE_ERROR",
    message: String(error),
    input: json,
  }),
);

A JSON.parse left unwrapped inside a Result-returning function is the commonest way the signature stops being true.

Full code: examples/core.md

Pattern 5: match for exhaustive handling

Both sides answered in one expression, which is what makes it the natural converter at an outbound boundary.

export const match = <T, E, U>(
  result: Result<T, E>,
  handlers: { ok: (value: T) => U; err: (error: E) => U },
): U => (result.ok ? handlers.ok(result.value) : handlers.err(result.error));

const response = match(loadUser(id), {
  ok: (user) => ({ status: 200, body: user }),
  err: (error) => toHttpResponse(error),
});

Full code: examples/core.md

Pattern 6: Discriminated error unions

Each variant carries what its own handler needs, and the union names every way the function fails.

type UserError =
  | { readonly code: "NOT_FOUND"; readonly userId: string }
  | {
      readonly code: "VALIDATION_ERROR";
      readonly field: string;
      readonly message: string;
    }
  | { readonly code: "NETWORK_ERROR"; readonly statusCode: number };

if (!result.ok) {
  switch (result.error.code) {
    case "NOT_FOUND":
      return showMissing(result.error.userId);
    case "VALIDATION_ERROR":
      return highlightField(result.error.field);
    case "NETWORK_ERROR":
      return offerRetry();
  }
}

Adding a variant reddens every switch that does not handle it, which is the whole return on the discriminant.

Full code: examples/core.md

Pattern 7: Combining several Results

Fail-fast returns the first error; collect-all returns every one.

export const combine = <T, E>(results: Result<T, E>[]): Result<T[], E> => {
  const values: T[] = [];
  for (const result of results) {
    if (!result.ok) return result;
    values.push(result.value);
  }
  return ok(values);
};

Full code: examples/combining.md

</patterns>

<red_flags>

Red flags

Breaks at runtime:

  • Reading result.value without checking ok — undefined at the point of use, and a non-null assertion or a cast is what got it past the compiler.
  • A throwing call left unwrapped inside a Result-returning function — the exception escapes a caller who was told there was nothing to catch.
  • Treating an error object as instanceof Error — a plain discriminated object is not one, so an instanceof check silently takes the wrong branch.

Surprising behaviour:

  • Discarding a Result compiles cleanly. Nothing in the type system marks the failure you dropped.
  • map with a function that itself returns a Result gives Result<Result<T, F>, E> — it type-checks, and the caller has to unwrap twice to reach anything. That doubling is what flatMap exists to prevent.
  • flatMap unions error types, so a long chain ends with an error union nobody wants to handle — narrow it with mapError at the point the extra variants stop mattering.
  • Result<void, E> rather than Result<undefined, E> for an operation with no success value; the second forces callers to name a value that does not exist.
  • A Promise<Result<T, E>> is truthy while it is pending, so an unawaited one passes an ok check that means nothing.
  • Rethrowing at a boundary throws the typed error away — the reason the caller could have branched on becomes a string.
  • combineWithAllErrors returning an array whose first element is all anyone displays wastes the work; either show them all or fail fast.
  • A pre-created error constant saves an allocation and loses the context that would have gone in it.

</red_flags>

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Hugging Face Inference SDK patterns for TypeScript/Node.js — InferenceClient setup, chat completion, text generation, streaming, embeddings, image generation, audio transcription, translation, summarization, and Inference Endpoints

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

LiteLLM proxy server setup, TypeScript client patterns via OpenAI SDK, model routing, fallbacks, load balancing, spend tracking, virtual keys, and production deployment

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

Serverless GPU compute platform for AI model deployment — web endpoints, GPU functions, model serving, and TypeScript client patterns

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

Local LLM inference with the Ollama JavaScript client -- chat, streaming, tool calling, vision, embeddings, structured output, model management, and OpenAI-compatible endpoint

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

Replicate SDK patterns for TypeScript/Node.js -- client setup, predictions, streaming, webhooks, file handling, model versioning, deployments, and training

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

Together AI SDK patterns for TypeScript — client setup, chat completions, streaming, structured output, function calling, embeddings, image generation, fine-tuning, and OpenAI-compatible endpoints

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

See all skills by agents-inc

Report a problem with this skill