--- id: wiki-2026-0508-discriminated-unions-for-error-h title: Discriminated Unions for Error Handling category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Tagged Unions, Result Type, Sum Types] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [typescript, error-handling, type-system, functional] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript framework: none --- # Discriminated Unions for Error Handling ## 매 한 줄 > **"매 throw 의 의 X — return value 의 success/failure 의 model"**. ML/Haskell `Either`, Rust `Result` 의 TypeScript port. 2026 의 매 typed exception 의 alternative — exhaustive checking + traceable failure path. ## 매 핵심 ### 매 왜 throw 의 X 의 - `throw` 의 type-erased — caller 의 매 catch 의 type 의 unknown. - Control flow 의 invisible — 매 implicit goto. - async 의 매 unhandled rejection 의 silent. ### 매 discriminator tag - 매 literal field (`type`, `tag`, `_tag`, `kind`) 의 매 union 의 narrow. - TypeScript 의 control-flow analysis 의 `if (r.type === "ok")` 의 narrow. - exhaustive check 의 `switch` + `never` 의 가능. ### 매 응용 1. API client (network/parse/validation 의 error 의 분리). 2. Form validation (field-level error). 3. Parser combinator (success/fail with location). 4. State machine (state 의 each 의 tag). ## 💻 패턴 ### 기본 Result type ```ts type Result = | { ok: true; value: T } | { ok: false; error: E }; const ok = (value: T): Result => ({ ok: true, value }); const err = (error: E): Result => ({ ok: false, error }); ``` ### Domain error 의 tagged ```ts type FetchUserError = | { type: "network"; cause: unknown } | { type: "not_found"; userId: string } | { type: "unauthorized" } | { type: "parse"; raw: unknown; issue: string }; async function fetchUser(id: string): Promise> { let res: Response; try { res = await fetch(`/api/users/${id}`); } catch (cause) { return err({ type: "network", cause }); } if (res.status === 404) return err({ type: "not_found", userId: id }); if (res.status === 401) return err({ type: "unauthorized" }); const raw = await res.json(); const parsed = UserSchema.safeParse(raw); if (!parsed.success) { return err({ type: "parse", raw, issue: parsed.error.message }); } return ok(parsed.data); } ``` ### Exhaustive handling ```ts function renderError(e: FetchUserError): string { switch (e.type) { case "network": return "Network failure. Retry?"; case "not_found": return `User ${e.userId} 의 X.`; case "unauthorized": return "Login 의 필요."; case "parse": return `Bad response: ${e.issue}`; default: { const _exhaustive: never = e; // 매 compile error 의 새 case 의 추가 시 return _exhaustive; } } } ``` ### Caller 의 narrow 의 사용 ```ts const r = await fetchUser("abc"); if (!r.ok) { // r.error: FetchUserError — 매 fully typed return renderError(r.error); } // r.value: User — 매 narrowed console.log(r.value.email); ``` ### Combinator (map / flatMap) ```ts function map(r: Result, f: (t: T) => U): Result { return r.ok ? ok(f(r.value)) : r; } function flatMap(r: Result, f: (t: T) => Result): Result { return r.ok ? f(r.value) : r; } // 매 chained pipeline const result = flatMap( await fetchUser(id), (u) => u.verified ? ok(u) : err({ type: "unauthorized" } as FetchUserError), ); ``` ### Form validation ```ts type FieldResult = | { state: "valid"; value: T } | { state: "invalid"; reasons: string[] } | { state: "pending" }; function validateEmail(s: string): FieldResult { if (s.length === 0) return { state: "pending" }; const reasons: string[] = []; if (!s.includes("@")) reasons.push("@ missing"); if (s.length > 254) reasons.push("Too long"); return reasons.length ? { state: "invalid", reasons } : { state: "valid", value: s }; } ``` ### Zod 의 native return ```ts import { z } from "zod"; const UserSchema = z.object({ id: z.string(), email: z.email() }); const parsed = UserSchema.safeParse(input); // parsed: { success: true; data: User } | { success: false; error: ZodError } ``` ### neverthrow 라이브러리 (2026 standard) ```ts import { ok, err, Result, ResultAsync } from "neverthrow"; const fetchUserSafe = (id: string): ResultAsync => ResultAsync.fromPromise(fetch(`/api/users/${id}`), (c) => ({ type: "network", cause: c } as const)) .andThen((res) => res.ok ? ResultAsync.fromSafePromise(res.json()) : err({ type: "not_found", userId: id } as const) ); ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Domain logic 의 expected failure | Discriminated union Result | | Truly exceptional (OOM, bug) | Throw | | Async I/O | `Promise>` 의 neverthrow `ResultAsync` | | Schema parsing | Zod `safeParse` | | Multi-step pipeline | `flatMap` chain 의 Effect TS | **기본값**: domain error 의 tagged union, infrastructure error 의 throw. ## 🔗 Graph - 부모: [[TypeScript]] · [[TypeScript 타입 시스템 (TypeScript Type System)|Type System]] - 변형: [[Effect TS 및 ts-brand 라이브러리 활용]] - 응용: [[Error Boundaries]] - Adjacent: [[Zod]] · [[Pattern Matching]] ## 🤖 LLM 활용 **언제**: domain error 의 modeling, exhaustive switch 의 enforce, async error path 의 explicit. **언제 X**: panic-level error (assertion fail) — throw 의 더 적합. ## ❌ 안티패턴 - **`{ error: string | null; data: T | null }`**: 매 implicit invariant — 둘 모두 null/non-null 의 type 의 allow. - **String-only error**: 매 i18n / programmatic handling 의 X. - **Discriminator 의 `boolean`**: `ok: true/false` 의 OK, but multi-variant 의 X — string literal 의 사용. - **Throw inside Result-returning fn**: 매 hybrid 의 worst. ## 🧪 검증 / 중복 - Verified (TypeScript handbook, Effect-TS docs, neverthrow, Rust `Result`, fp-ts `Either`). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — exhaustive check + neverthrow + combinators 추가 |