{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/fp-ts-errors",
  "version": "1.0.0",
  "name": "Fp Ts Errors",
  "description": "Handle errors as values using fp-ts Either and TaskEither for cleaner, more predictable TypeScript code. Use when implementing error handling patterns with fp-ts.",
  "system_prompt_fragment": "# Practical Error Handling with fp-ts\n\nThis skill teaches you how to handle errors without try/catch spaghetti. No academic jargon - just practical patterns for real problems.\n\n## When to Use This Skill\n\n- When you want type-safe error handling in TypeScript\n- When replacing try/catch with Either and TaskEither patterns\n- When building APIs or services that need explicit error types\n- When accumulating multiple validation errors\n\nThe core idea: **Errors are just data**. Instead of throwing them into the void and hoping someone catches them, return them as values that TypeScript can track.\n\n---\n\n## 1. Stop Throwing Everywhere\n\n### The Problem with Exceptions\n\nExceptions are invisible in your types. They break the contract between functions.\n\n```typescript\n// What this function signature promises:\nfunction getUser(id: string): User\n\n// What it actually does:\nfunction getUser(id: string): User {\n  if (!id) throw new Error('ID required')\n  const user = db.find(id)\n  if (!user) throw new Error('User not found')\n  return user\n}\n\n// The caller has no idea this can fail\nconst user = getUser(id) // Might explode!\n```\n\nYou end up with code like this:\n\n```typescript\n// MESSY: try/catch everywhere\nfunction processOrder(orderId: string) {\n  let order\n  try {\n    order = getOrder(orderId)\n  } catch (e) {\n    console.error('Failed to get order')\n    return null\n  }\n\n  let user\n  try {\n    user = getUser(order.userId)\n  } catch (e) {\n    console.error('Failed to get user')\n    return null\n  }\n\n  let payment\n  try {\n    payment = chargeCard(user.cardId, order.total)\n  } catch (e) {\n    console.error('Payment failed')\n    return null\n  }\n\n  return { order, user, payment }\n}\n```\n\n### The Solution: Return Errors as Values\n\n```typescript\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\n\n// Now TypeScript KNOWS this can fail\nfunction getUser(id: string): E.Either<string, User> {\n  if (!id) return E.left('ID required')\n  const user = db.find(id)\n  if (!user) return E.left('User not found')\n  return E.right(user)\n}\n\n// The caller is forced to handle both cases\nconst result = getUser(id)\n// result is Either<string, User> - error OR success, never both\n```\n\n---\n\n## 2. The Result Pattern (Either)\n\n`Either<E, A>` is simple: it holds either an error (`E`) or a value (`A`).\n\n- `Left` = error case\n- `Right` = success case (think \"right\" as in \"correct\")\n\n```typescript\nimport * as E from 'fp-ts/Either'\n\n// Creating values\nconst success = E.right(42)           // Right(42)\nconst failure = E.left('Oops')        // Left('Oops')\n\n// Checking what you have\nif (E.isRight(result)) {\n  console.log(result.right) // The success value\n} else {\n  console.log(result.left)  // The error\n}\n\n// Better: pattern match with fold\nconst message = pipe(\n  result,\n  E.fold(\n    (error) => `Failed: ${error}`,\n    (value) => `Got: ${value}`\n  )\n)\n```\n\n### Converting Throwing Code to Either\n\n```typescript\n// Wrap any throwing function with tryCatch\nconst parseJSON = (json: string): E.Either<Error, unknown> =>\n  E.tryCatch(\n    () => JSON.parse(json),\n    (e) => (e instanceof Error ? e : new Error(String(e)))\n  )\n\nparseJSON('{\"valid\": true}')  // Right({ valid: true })\nparseJSON('not json')          // Left(SyntaxError: ...)\n\n// For functions you'll reuse, use tryCatchK\nconst safeParseJSON = E.tryCatchK(\n  JSON.parse,\n  (e) => (e instanceof Error ? e : new Error(String(e)))\n)\n```\n\n### Common Either Operations\n\n```typescript\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\n\n// Transform the success value\nconst doubled = pipe(\n  E.right(21),\n  E.map(n => n * 2)\n) // Right(42)\n\n// Transform the error\nconst betterError = pipe(\n  E.left('bad'),\n  E.mapLeft(e => `Error: ${e}`)\n) // Left('Error: bad')\n\n// Provide a default for errors\nconst value = pipe(\n  E.left('failed'),\n  E.getOrElse(() => 0)\n) // 0\n\n// Convert nullable to Either\nconst fromNullable = E.fromNullable('not found')\nfromNullable(user)  // Right(user) if exists, Left('not found') if null/undefined\n```\n\n---\n\n## 3. Chaining Operations That Might Fail\n\nThe real power comes from chaining. Each step can fail, but you write it as a clean pipeline.\n\n### Before: Nested Try/Catch Hell\n\n```typescript\n// MESSY: Each step can fail, nested try/catch everywhere\nfunction processUserOrder(userId: string, productId: string): Result | null {\n  let user\n  try {\n    user = getUser(userId)\n  } catch (e) {\n    logError('User fetch failed', e)\n    return null\n  }\n\n  if (!user.isActive) {\n    logError('User not active')\n    return null\n  }\n\n  let product\n  try {\n    product = getProduct(productId)\n  } catch (e) {\n    logError('Product fetch failed', e)\n    return null\n  }\n\n  if (product.stock < 1) {\n    logError('Out of stock')\n    return null\n  }\n\n  let order\n  try {\n    order = createOrder(user, product)\n  } catch (e) {\n    logError('Order creation failed', e)\n    return null\n  }\n\n  return order\n}\n```\n\n### After: Clean Chain with Either\n\n```typescript\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\n\n// Each function returns Either<Error, T>\nconst getUser = (id: string): E.Either<string, User> => { ... }\nconst getProduct = (id: string): E.Either<string, Product> => { ... }\nconst createOrder = (user: User, product: Product): E.Either<string, Order> => { ... }\n\n// Chain them together - first error stops the chain\nconst processUserOrder = (userId: string, productId: string): E.Either<string, Order> =>\n  pipe(\n    getUser(userId),\n    E.filterOrElse(\n      user => user.isActive,\n      () => 'User not active'\n    ),\n    E.chain(user =>\n      pipe(\n        getProduct(productId),\n        E.filterOrElse(\n          product => product.stock >= 1,\n          () => 'Out of stock'\n        ),\n        E.chain(product => createOrder(user, product))\n      )\n    )\n  )\n\n// Or use Do notation for cleaner access to intermediate values\nconst processUserOrder = (userId: string, productId: string): E.Either<string, Order> =>\n  pipe(\n    E.Do,\n    E.bind('user', () => getUser(userId)),\n    E.filterOrElse(\n      ({ user }) => user.isActive,\n      () => 'User not active'\n    ),\n    E.bind('product', () => getProduct(productId)),\n    E.filterOrElse(\n      ({ product }) => product.stock >= 1,\n      () => 'Out of stock'\n    ),\n    E.chain(({ user, product }) => createOrder(user, product))\n  )\n```\n\n### Different Error Types? Use chainW\n\n```typescript\ntype ValidationError = { type: 'validation'; message: string }\ntype DbError = { type: 'db'; message: string }\n\nconst validateInput = (id: string): E.Either<ValidationError, string> => { ... }\nconst fetchFromDb = (id: string): E.Either<DbError, User> => { ... }\n\n// chainW (W = \"wider\") automatically unions the error types\nconst process = (id: string): E.Either<ValidationError | DbError, User> =>\n  pipe(\n    validateInput(id),\n    E.chainW(validId => fetchFromDb(validId))\n  )\n```\n\n---\n\n## 4. Collecting Multiple Errors\n\nSometimes you want ALL errors, not just the first one. Form validation is the classic example.\n\n### Before: Collecting Errors Manually\n\n```typescript\n// MESSY: Manual error accumulation\nfunction validateForm(form: FormData): { valid: boolean; errors: string[] } {\n  const errors: string[] = []\n\n  if (!form.email) {\n    errors.push('Email required')\n  } else if (!form.email.includes('@')) {\n    errors.push('Invalid email')\n  }\n\n  if (!form.password) {\n    errors.push('Password required')\n  } else if (form.password.length < 8) {\n    errors.push('Password too short')\n  }\n\n  if (!form.age) {\n    errors.push('Age required')\n  } else if (form.age < 18) {\n    errors.push('Must be 18+')\n  }\n\n  return { valid: errors.length === 0, errors }\n}\n```\n\n### After: Validation with Error Accumulation\n\n```typescript\nimport * as E from 'fp-ts/Either'\nimport * as NEA from 'fp-ts/NonEmptyArray'\nimport { sequenceS } from 'fp-ts/Apply'\nimport { pipe } from 'fp-ts/function'\n\n// Errors as a NonEmptyArray (always at least one)\ntype Errors = NEA.NonEmptyArray<string>\n\n// Create the applicative that accumulates errors\nconst validation = E.getApplicativeValidation(NEA.getSemigroup<string>())\n\n// Validators that return Either<Errors, T>\nconst validateEmail = (email: string): E.Either<Errors, string> =>\n  !email ? E.left(NEA.of('Email required'))\n  : !email.includes('@') ? E.left(NEA.of('Invalid email'))\n  : E.right(email)\n\nconst validatePassword = (password: string): E.Either<Errors, string> =>\n  !password ? E.left(NEA.of('Password required'))\n  : password.length < 8 ? E.left(NEA.of('Password too short'))\n  : E.right(password)\n\nconst validateAge = (age: number | undefined): E.Either<Errors, number> =>\n  age === undefined ? E.left(NEA.of('Age required'))\n  : age < 18 ? E.left(NEA.of('Must be 18+'))\n  : E.right(age)\n\n// Combine all validations - collects ALL errors\nconst validateForm = (form: FormData) =>\n  sequenceS(validation)({\n    email: validateEmail(form.email),\n    password: validatePassword(form.password),\n    age: validateAge(form.age)\n  })\n\n// Usage\nvalidateForm({ email: '', password: '123', age: 15 })\n// Left(['Email required', 'Password too short', 'Must be 18+'])\n\nvalidateForm({ email: 'a@b.com', password: 'longpassword', age: 25 })\n// Right({ email: 'a@b.com', password: 'longpassword', age: 25 })\n```\n\n### Field-Level Errors for Forms\n\n```typescript\ninterface FieldError {\n  field: string\n  message: string\n}\n\ntype FormErrors = NEA.NonEmptyArray<FieldError>\n\nconst fieldError = (field: string, message: string): FormErrors =>\n  NEA.of({ field, message })\n\nconst formValidation = E.getApplicativeValidation(NEA.getSemigroup<FieldError>())\n\n// Now errors know which field they belong to\nconst validateEmail = (email: string): E.Either<FormErrors, string> =>\n  !email ? E.left(fieldError('email', 'Required'))\n  : !email.includes('@') ? E.left(fieldError('email', 'Invalid format'))\n  : E.right(email)\n\n// Easy to display in UI\nconst getFieldError = (errors: FormErrors, field: string): string | undefined =>\n  errors.find(e => e.field === field)?.message\n```\n\n---\n\n## 5. Async Operations (TaskEither)\n\nFor async operations that can fail, use `TaskEither`. It's like `Either` but for promises.\n\n- `TaskEither<E, A>` = a function that returns `Promise<Either<E, A>>`\n- Lazy: nothing runs until you execute it\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither'\nimport { pipe } from 'fp-ts/function'\n\n// Wrap any async operation\nconst fetchUser = (id: string): TE.TaskEither<Error, User> =>\n  TE.tryCatch(\n    () => fetch(`/api/users/${id}`).then(r => r.json()),\n    (e) => (e instanceof Error ? e : new Error(String(e)))\n  )\n\n// Chain async operations - just like Either\nconst getUserPosts = (userId: string): TE.TaskEither<Error, Post[]> =>\n  pipe(\n    fetchUser(userId),\n    TE.chain(user => fetchPosts(user.id))\n  )\n\n// Execute when ready\nconst result = await getUserPosts('123')() // Returns Either<Error, Post[]>\n```\n\n### Before: Promise Chain with Error Handling\n\n```typescript\n// MESSY: try/catch mixed with promise chains\nasync function loadDashboard(userId: string) {\n  try {\n    const user = await fetchUser(userId)\n    if (!user) throw new Error('User not found')\n\n    let posts, notifications, settings\n    try {\n      [posts, notifications, settings] = await Promise.all([\n        fetchPosts(user.id),\n        fetchNotifications(user.id),\n        fetchSettings(user.id)\n      ])\n    } catch (e) {\n      // Which one failed? Who knows!\n      console.error('Failed to load data', e)\n      return null\n    }\n\n    return { user, posts, notifications, settings }\n  } catch (e) {\n    console.error('Failed to load user', e)\n    return null\n  }\n}\n```\n\n### After: Clean TaskEither Pipeline\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither'\nimport { sequenceS } from 'fp-ts/Apply'\nimport { pipe } from 'fp-ts/function'\n\nconst loadDashboard = (userId: string) =>\n  pipe(\n    fetchUser(userId),\n    TE.chain(user =>\n      pipe(\n        // Parallel fetch with sequenceS\n        sequenceS(TE.ApplyPar)({\n          posts: fetchPosts(user.id),\n          notifications: fetchNotifications(user.id),\n          settings: fetchSettings(user.id)\n        }),\n        TE.map(data => ({ user, ...data }))\n      )\n    )\n  )\n\n// Execute and handle both cases\npipe(\n  loadDashboard('123'),\n  TE.fold(\n    (error) => T.of(renderError(error)),\n    (data) => T.of(renderDashboard(data))\n  )\n)()\n```\n\n### Retry Failed Operations\n\n```typescript\nimport * as T from 'fp-ts/Task'\nimport * as TE from 'fp-ts/TaskEither'\nimport { pipe } from 'fp-ts/function'\n\nconst retry = <E, A>(\n  task: TE.TaskEither<E, A>,\n  attempts: number,\n  delayMs: number\n): TE.TaskEither<E, A> =>\n  pipe(\n    task,\n    TE.orElse((error) =>\n      attempts > 1\n        ? pipe(\n            T.delay(delayMs)(T.of(undefined)),\n            T.chain(() => retry(task, attempts - 1, delayMs * 2))\n          )\n        : TE.left(error)\n    )\n  )\n\n// Retry up to 3 times with exponential backoff\nconst fetchWithRetry = retry(fetchUser('123'), 3, 1000)\n```\n\n### Fallback to Alternative\n\n```typescript\n// Try cache first, fall back to API\nconst getUserData = (id: string) =>\n  pipe(\n    fetchFromCache(id),\n    TE.orElse(() => fetchFromApi(id)),\n    TE.orElse(() => TE.right(defaultUser)) // Last resort default\n  )\n```\n\n---\n\n## 6. Converting Between Patterns\n\nReal codebases have throwing functions, nullable values, and promises. Here's how to work with them.\n\n### From Nullable to Either\n\n```typescript\nimport * as E from 'fp-ts/Either'\nimport * as O from 'fp-ts/Option'\n\n// Direct conversion\nconst user = users.find(u => u.id === id) // User | undefined\nconst result = E.fromNullable('User not found')(user)\n\n// From Option\nconst maybeUser: O.Option<User> = O.fromNullable(user)\nconst eitherUser = pipe(\n  maybeUser,\n  E.fromOption(() => 'User not found')\n)\n```\n\n### From Throwing Function to Either\n\n```typescript\n// Wrap at the boundary\nconst safeParse = <T>(schema: ZodSchema<T>) => (data: unknown): E.Either<ZodError, T> =>\n  E.tryCatch(\n    () => schema.parse(data),\n    (e) => e as ZodError\n  )\n\n// Use throughout your code\nconst parseUser = safeParse(UserSchema)\nconst result = parseUser(rawData) // Either<ZodError, User>\n```\n\n### From Promise to TaskEither\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither'\n\n// Wrap external async functions\nconst fetchJson = <T>(url: string): TE.TaskEither<Error, T> =>\n  TE.tryCatch(\n    () => fetch(url).then(r => r.json()),\n    (e) => new Error(`Fetch failed: ${e}`)\n  )\n\n// Wrap axios, prisma, any async library\nconst getUserFromDb = (id: string): TE.TaskEither<DbError, User> =>\n  TE.tryCatch(\n    () => prisma.user.findUniqueOrThrow({ where: { id } }),\n    (e) => ({ code: 'DB_ERROR', cause: e })\n  )\n```\n\n### Back to Promise (Escape Hatch)\n\nSometimes you need a plain Promise for external APIs.\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither'\nimport * as E from 'fp-ts/Either'\n\nconst myTaskEither: TE.TaskEither<Error, User> = fetchUser('123')\n\n// Option 1: Get the Either (preserves both cases)\nconst either: E.Either<Error, User> = await myTaskEither()\n\n// Option 2: Throw on error (for legacy code)\nconst toThrowingPromise = <E, A>(te: TE.TaskEither<E, A>): Promise<A> =>\n  te().then(E.fold(\n    (error) => Promise.reject(error),\n    (value) => Promise.resolve(value)\n  ))\n\nconst user = await toThrowingPromise(fetchUser('123')) // Throws if Left\n\n// Option 3: Default on error\nconst user = await pipe(\n  fetchUser('123'),\n  TE.getOrElse(() => T.of(defaultUser))\n)()\n```\n\n---\n\n## Real Scenarios\n\n### Parse User Input Safely\n\n```typescript\ninterface ParsedInput {\n  id: number\n  name: string\n  tags: string[]\n}\n\nconst parseInput = (raw: unknown): E.Either<string, ParsedInput> =>\n  pipe(\n    E.Do,\n    E.bind('obj', () =>\n      typeof raw === 'object' && raw !== null\n        ? E.right(raw as Record<string, unknown>)\n        : E.left('Input must be an object')\n    ),\n    E.bind('id', ({ obj }) =>\n      typeof obj.id === 'number'\n        ? E.right(obj.id)\n        : E.left('id must be a number')\n    ),\n    E.bind('name', ({ obj }) =>\n      typeof obj.name === 'string' && obj.name.length > 0\n        ? E.right(obj.name)\n        : E.left('name must be a non-empty string')\n    ),\n    E.bind('tags', ({ obj }) =>\n      Array.isArray(obj.tags) && obj.tags.every(t => typeof t === 'string')\n        ? E.right(obj.tags as string[])\n        : E.left('tags must be an array of strings')\n    ),\n    E.map(({ id, name, tags }) => ({ id, name, tags }))\n  )\n\n// Usage\nparseInput({ id: 1, name: 'test', tags: ['a', 'b'] })\n// Right({ id: 1, name: 'test', tags: ['a', 'b'] })\n\nparseInput({ id: 'wrong', name: '', tags: null })\n// Left('id must be a number')\n```\n\n### API Call with Full Error Handling\n\n```typescript\ninterface ApiError {\n  code: string\n  message: string\n  status?: number\n}\n\nconst createApiError = (message: string, code = 'UNKNOWN', status?: number): ApiError =>\n  ({ code, message, status })\n\nconst fetchWithErrorHandling = <T>(url: string): TE.TaskEither<ApiError, T> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch(url),\n      () => createApiError('Network error', 'NETWORK')\n    ),\n    TE.chain(response =>\n      response.ok\n        ? TE.tryCatch(\n            () => response.json() as Promise<T>,\n            () => createApiError('Invalid JSON', 'PARSE')\n          )\n        : TE.left(createApiError(\n            `HTTP ${response.status}`,\n            response.status === 404 ? 'NOT_FOUND' : 'HTTP_ERROR',\n            response.status\n          ))\n    )\n  )\n\n// Usage with pattern matching on error codes\nconst handleUserFetch = (userId: string) =>\n  pipe(\n    fetchWithErrorHandling<User>(`/api/users/${userId}`),\n    TE.fold(\n      (error) => {\n        switch (error.code) {\n          case 'NOT_FOUND': return T.of(showNotFoundPage())\n          case 'NETWORK': return T.of(showOfflineMessage())\n          default: return T.of(showGenericError(error.message))\n        }\n      },\n      (user) => T.of(showUserProfile(user))\n    )\n  )\n```\n\n### Process List Where Some Items Might Fail\n\n```typescript\nimport * as A from 'fp-ts/Array'\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\n\ninterface ProcessResult<T> {\n  successes: T[]\n  failures: Array<{ item: unknown; error: string }>\n}\n\n// Process all, collect successes and failures separately\nconst processAllCollectErrors = <T, R>(\n  items: T[],\n  process: (item: T) => E.Either<string, R>\n): ProcessResult<R> => {\n  const results = items.map((item, index) =>\n    pipe(\n      process(item),\n      E.mapLeft(error => ({ item, error, index }))\n    )\n  )\n\n  return {\n    successes: pipe(results, A.filterMap(E.toOption)),\n    failures: pipe(\n      results,\n      A.filterMap(r => E.isLeft(r) ? O.some(r.left) : O.none)\n    )\n  }\n}\n\n// Usage\nconst parseNumbers = (inputs: string[]) =>\n  processAllCollectErrors(inputs, input => {\n    const n = parseInt(input, 10)\n    return isNaN(n) ? E.left(`Invalid number: ${input}`) : E.right(n)\n  })\n\nparseNumbers(['1', 'abc', '3', 'def'])\n// {\n//   successes: [1, 3],\n//   failures: [\n//     { item: 'abc', error: 'Invalid number: abc', index: 1 },\n//     { item: 'def', error: 'Invalid number: def', index: 3 }\n//   ]\n// }\n```\n\n### Bulk Operations with Partial Success\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither'\nimport * as T from 'fp-ts/Task'\nimport { pipe } from 'fp-ts/function'\n\ninterface BulkResult<T> {\n  succeeded: T[]\n  failed: Array<{ id: string; error: string }>\n}\n\nconst bulkProcess = <T>(\n  ids: string[],\n  process: (id: string) => TE.TaskEither<string, T>\n): T.Task<BulkResult<T>> =>\n  pipe(\n    ids,\n    A.map(id =>\n      pipe(\n        process(id),\n        TE.fold(\n          (error) => T.of({ type: 'failed' as const, id, error }),\n          (result) => T.of({ type: 'succeeded' as const, result })\n        )\n      )\n    ),\n    T.sequenceArray,\n    T.map(results => ({\n      succeeded: results\n        .filter((r): r is { type: 'succeeded'; result: T } => r.type === 'succeeded')\n        .map(r => r.result),\n      failed: results\n        .filter((r): r is { type: 'failed'; id: string; error: string } => r.type === 'failed')\n        .map(({ id, error }) => ({ id, error }))\n    }))\n  )\n\n// Usage\nconst deleteUsers = (userIds: string[]) =>\n  bulkProcess(userIds, id =>\n    pipe(\n      deleteUser(id),\n      TE.mapLeft(e => e.message)\n    )\n  )\n\n// All operations run, you get a report of what worked and what didn't\n```\n\n---\n\n## Quick Reference\n\n| Pattern | Use When | Example |\n|---------|----------|---------|\n| `E.right(value)` | Creating a success | `E.right(42)` |\n| `E.left(error)` | Creating a failure | `E.left('not found')` |\n| `E.tryCatch(fn, onError)` | Wrapping throwing code | `E.tryCatch(() => JSON.parse(s), toError)` |\n| `E.fromNullable(error)` | Converting nullable | `E.fromNullable('missing')(maybeValue)` |\n| `E.map(fn)` | Transform success | `pipe(result, E.map(x => x * 2))` |\n| `E.mapLeft(fn)` | Transform error | `pipe(result, E.mapLeft(addContext))` |\n| `E.chain(fn)` | Chain operations | `pipe(getA(), E.chain(a => getB(a.id)))` |\n| `E.chainW(fn)` | Chain with different error type | `pipe(validate(), E.chainW(save))` |\n| `E.fold(onError, onSuccess)` | Handle both cases | `E.fold(showError, showData)` |\n| `E.getOrElse(onError)` | Extract with default | `E.getOrElse(() => 0)` |\n| `E.filterOrElse(pred, onFalse)` | Validate with error | `E.filterOrElse(x => x > 0, () => 'must be positive')` |\n| `sequenceS(validation)({...})` | Collect all errors | Form validation |\n\n### TaskEither Equivalents\n\nAll Either operations have TaskEither equivalents:\n- `TE.right`, `TE.left`, `TE.tryCatch`\n- `TE.map`, `TE.mapLeft`, `TE.chain`, `TE.chainW`\n- `TE.fold`, `TE.getOrElse`, `TE.filterOrElse`\n- `TE.orElse` for fallbacks\n\n---\n\n## Summary\n\n1. **Return errors as values** - Use Either/TaskEither instead of throwing\n2. **Chain with confidence** - `chain` stops at first error automatically\n3. **Collect all errors when needed** - Use validation applicative for forms\n4. **Wrap at boundaries** - Convert throwing/Promise code at the edges\n5. **Match at the end** - Use `fold` to handle both cases when you're ready to act\n\nThe payoff: TypeScript tracks your errors, no more forgotten try/catch, clear control flow, and composable error handling.",
  "applicable_domains": [
    "other"
  ],
  "category": "other",
  "invocation": [
    "/fp-ts-errors"
  ],
  "authored_by": "claudeskills.in community",
  "source_url": "https://claudeskills.in/skill/fp-ts-errors",
  "provenance": {
    "source": "claudeskills.in",
    "source_url": "https://claudeskills.in/skill/fp-ts-errors",
    "license": "unknown",
    "imported_at": "2026-09-03",
    "notes": "Aggregated by claudeskills.in from community GitHub lists. Upstream as recorded by the aggregator: https://github.com/whatiskadudoing/fp-ts-skills."
  },
  "tags": [
    "claudeskills",
    "other",
    "risk-reviewed"
  ],
  "lifecycle": "draft"
}