Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Web Error Handling Result Types

ASecurity

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

24 stars
0 votes
0 copies
0 views
Added 5/29/2026
ai-agentstypescriptgoexpressapidatabase

Works with

api

Security Analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned 9/21/2026

$npx -y skills add agents-inc/skills --skill web-error-handling-result-types --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Web Error Handling Result Types?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Web Error Handling Result Types
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agents-inc-web-error-handling-result-types/badge)](https://www.skillsdirectory.com/skills/agents-inc-web-error-handling-result-types)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: web-error-handling-result-types
description: TypeScript Result/Either types for type-safe error handling, railway-oriented programming patterns, error as values
---

# 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](examples/core.md) — the Result module, typed error definitions, wrapping throwing code, pattern matching
- [examples/async.md](examples/async.md) — `Promise<Result>`, async chaining, retry, converting a promise
- [examples/combining.md](examples/combining.md) — fail-fast, collect-all, object and sequential combination
- [reference.md](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](examples/core.md) is the whole file.
- **A library owns the type** — the operations are named differently but compose identically;
  [reference.md](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](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](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.

```typescript
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](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.

```typescript
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.

```typescript
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](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.

```typescript
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](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.

```typescript
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](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.

```typescript
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](examples/core.md)

### Pattern 7: Combining several Results

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

```typescript
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](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>

Attribution

agents-incagents-inc
View sourceSee grades on GitHubMore from agents-inc →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less tokens". Stays on until "stop caveman" or "normal mode".

1100021 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

698621 votes

Writing Skills

Create and manage Claude Code skills in HASH repository following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns), UserPromptSubmit hook, and the 500-line rule. Includes validation and debugging with SKILL_DEBUG. Examples include rust-error-stack, cargo-dep...

3931 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3421 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

741 votes
View all in ai-agents →