Skip to content
Sarj Code Standards

prefer-coded-error-result

Return a stable error code and typed context instead of only a rendered failure message.

Why

A message-only or opaque failure hides the identity needed for repair, conflict handling, localization and exhaustive dispatch. Preserve a finite error code and the minimum handling context; let the consumer format its message. Reuse the native no-explicit-any rule, compiler narrowing and type-aware exhaustiveness checks.

Fix

Use the existing Result or outcome union with a finite error code and variant-specific context. Preserve public behavior through a presentation adapter, and handle or propagate each relevant variant without parsing message text.

Examples

Before — flagged A transparent alias must preserve the library error contract
app/result.ts
import type { Result as NativeResult } from "neverthrow";
type Saved<T, E> = Promise<NativeResult<T, E>>;
export type Output = Saved<number, string>;
After — preferred Keep a finite error code and only the context its consumer needs
app/result.ts
import type { Result as NativeResult } from "neverthrow";
type Saved<T, E> = Promise<NativeResult<T, E>>;
export type Output = Saved<
number,
{ code: "duplicate"; name: string } | { code: "denied" }
>;
Before — flagged A failure erases identity into a message
app/result.ts
export type Result<T> = { ok: true; data: T } | { ok: false; message: string };
After — preferred The consumer can choose behavior and format context
app/result.ts
type Error = { code: "duplicate"; name: string } | { code: "invalid_transition" };
export type Result<T> = { ok: true; data: T } | { ok: false; error: Error };
Before — flagged A conditional return still needs a useful error contract
app/result.ts
import { err, ok } from "neverthrow";
export const save = (name: string, allowed: boolean) =>
allowed ? ok(name) : err(`Denied ${name}`);
After — preferred Preserve the code and context and let the consumer format
app/result.ts
import { err, ok, type Result } from "neverthrow";
type Denied = { code: "denied"; name: string };
export function save(name: string, allowed: boolean): Result<string, Denied> {
return allowed ? ok(name) : err({ code: "denied", name });
}
export function format(error: Denied): string {
return `Denied ${error.name}`;
}
Before — flagged A code alone does not move message formatting to the consumer
app/result.ts
export function reject(name: string) {
return { ok: false, code: "duplicate", message: `Duplicate ${name}` };
}
After — preferred Retain the typed context and format after handling the code
app/result.ts
type Duplicate = { code: "duplicate"; name: string };
export function reject(name: string): { ok: false; error: Duplicate } {
return { ok: false, error: { code: "duplicate", name } };
}
export function format(error: Duplicate): string {
return `Duplicate ${error.name}`;
}
Before — flagged Conventional success and failure tags establish one Result
app/result.ts
export type Outcome<T> =
{ status: "success"; value: T } | { status: "failure"; reason: string };
After — preferred A loading report can preserve an unconfigured absence state
app/result.ts
export type Source<T> =
| { status: "loaded"; value: T }
| { status: "failed" | "unconfigured"; reason: string };
Before — flagged An exclusive failure still needs an actionable identity
app/result.ts
export type Outcome<T> = { ok: true; data: T } | { ok: false; error: string };
After — preferred Partial data and its independent error remain separate outcomes
app/result.ts
export type Partial<T> =
{ ok: true; data: T; error?: string } | { ok: false; data: T; error: string };
Before — flagged A concrete generic error channel erases its identity
app/result.ts
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
export type Saved = Result<number, string>;
After — preferred An error code needs no invented payload
app/result.ts
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
export type Saved = Result<number, "duplicate" | "invalid_transition">;
Before — flagged A proven Result constructor does not make formatted text actionable
app/result.ts
import { err } from "neverthrow";
export function reject(name: string) {
return err(`Missing ${name}`);
}
After — preferred Reuse the established Result library with a typed domain error
app/result.ts
import { err, type Result } from "neverthrow";
type Missing = { code: "missing"; name: string };
export function reject(name: string): Result<never, Missing> {
return err({ code: "missing", name });
}
Before — flagged A failure-only Error-suffixed field can still erase its cases
app/result.ts
export type Outcome<T> = { ok: true; value: T } | { ok: false; fallbackError: string };
After — preferred Preserve the existing field name with the required context
app/result.ts
export type Outcome<T> =
| { ok: true; value: T }
| { ok: false; fallbackError: { code: "unsupported"; requestedKind: string } };
Before — flagged Unknown preserves the need to narrow but hides the domain's cases
app/result.ts
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
export type Saved = Result<number, unknown>;
After — preferred Expose the domain's rejection case without inventing context
app/result.ts
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
export type Saved = Result<number, { code: "unavailable" }>;
Before — flagged An error-presence check still needs a useful error contract
app/result.ts
export type Outcome<T> = { value: T } | { error: string; field?: "name" };
After — preferred Retain the existing presence check with a finite error union
app/result.ts
type SaveError = { code: "duplicate"; name: string } | { code: "denied" };
export type Outcome<T> = { value: T } | { error: SaveError };
Before — flagged An outcome tag distinguishes success from failure, not the failure's cause
app/result.ts
export type Outcome<T> = { kind: "ok"; value: T } | { kind: "error"; message: string };
After — preferred Preserve the established string tag and add the actionable error
app/result.ts
export type Outcome<T> =
| { kind: "ok"; value: T }
| { kind: "error"; error: { code: "duplicate"; name: string } };

References