web-error-handling-result-types-655005 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-error-handling-result-types-655005 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: AResult<T, E>is a discriminated union onok, so TypeScript refuses to readvalueuntil 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:
Promise<Result>, async chaining, retry, converting a promiseok, err, map, flatMap, match,tryCatch. examples/core.md is the whole file.
reference.md maps the names.
Promise<Result<T, E>> and the awaiting is thecaller's; see examples/async.md.
<critical_requirements>
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:
Handled elsewhere:
<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>
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 EA 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>
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
map and mapErrorEach 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.
flatMap for chainingThe 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
tryCatch at the boundaryThrowing 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
match for exhaustive handlingBoth 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
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
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>
Breaks at runtime:
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.instanceof Error — a plain discriminated object is not one, so an instanceof check silently takes the wrong branch.Surprising behaviour:
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.Promise<Result<T, E>> is truthy while it is pending, so an unawaited one passes an ok check that means nothing.combineWithAllErrors returning an array whose first element is all anyone displays wastes the work; either show them all or fail fast.</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.