{"id":"eebb21fa-7080-4d0c-bf96-b8931ea17392","shortId":"4p5rtv","kind":"skill","title":"fp-errors","tagline":"Stop throwing everywhere - handle errors as values using Either and TaskEither for cleaner, more predictable code","description":"# 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\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## When to Use\n- You need to replace exception-heavy code with `Either` or `TaskEither`.\n- The task involves validation, domain errors, or clearer error contracts in TypeScript.\n- You want pragmatic fp-ts error-handling guidance for real application code.\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.\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","tags":["errors","antigravity","awesome","skills","sickn33","agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity-skills"],"capabilities":["skill","source-sickn33","skill-fp-errors","topic-agent-skills","topic-agentic-skills","topic-ai-agent-skills","topic-ai-agents","topic-ai-coding","topic-ai-workflows","topic-antigravity","topic-antigravity-skills","topic-claude-code","topic-claude-code-skills","topic-codex-cli","topic-codex-skills"],"categories":["antigravity-awesome-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-errors","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add sickn33/antigravity-awesome-skills","source_repo":"https://github.com/sickn33/antigravity-awesome-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 34793 github stars · SKILL.md body (22,463 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-24T00:50:58.031Z","embedding":null,"createdAt":"2026-04-18T21:37:26.015Z","updatedAt":"2026-04-24T00:50:58.031Z","lastSeenAt":"2026-04-24T00:50:58.031Z","tsv":"'/api/users':1268,2055 '/apply':977,1411 '/array':2096 '/either':267,369,490,684,962,1601,1772,2104 '/function':274,497,691,984,1254,1418,1491,2111,2246 '/nonemptyarray':970 '/option':1609 '/task':1476,2239 '/taskeither':1247,1404,1484,1695,1764,2231 '0':539,540,947,1908,2449,2459 '1':115,646,755,802,1512,1522,1781,1952,1960,2194,2199,2209,2492 '10':2183 '1000':1541 '123':1100,1309,1455,1539,1779,1826,1840 '15':1102 '18':939,943,1067,1072,1111 '2':330,510,1524,1796,2404,2502 '21':506 '25':1118,1125 '3':561,1530,1540,1831,2196,2200,2217,2512 '4':867,2523 '404':2036 '42':375,377,512,2369 '5':1202,2533 '6':1569 '8':926,1044 'a.filtermap':2159,2164 'a.id':2422 'a.map':2271 'a@b.com':1114,1121 'abc':2195,2203,2207 'academ':39 'access':770 'accumul':895,953,1000 'act':2549 'activ':626,746,794 'actual':148 'addcontext':2412 'age':934,1054,1060,1064,1066,1074,1093,1101,1117,1124 'altern':1544 'alway':989 'api':1552,1755,1978 'apierror':1986,2001,2010 'applic':113,998,2520 'array':1939,2117,2252 'array.isarray':1923 'ask':2600 'async':1203,1207,1257,1283,1327,1698,1723 'attempt':1500,1511,1521 'automat':847,2511 'await':1307,1335,1354,1793,1823,1837 'axio':1720 'b':1957,1965 'back':1550,1742 'backoff':1534 'bad':520,527 'belong':1162 'better':401 'bettererror':517 'boolean':901 'boundari':1651,2526,2608 'break':130 'bulk':2218 'bulkprocess':2258,2334 'bulkresult':2248,2268 'cach':1547 'call':1979 'caller':177,309 'case':315,352,355,1452,1787,2061,2067,2439,2543 'catch':64,211,226,242,612,635,660,1362,1382 'caus':1740 'chain':562,572,673,721,728,1282,1316,1326,2415,2425,2503,2506 'chainw':816,844 'chargecard':239 'check':384 'clarif':2602 'classic':885 'clean':583,672,1393 'cleaner':16,769 'clear':2560,2575 'clearer':96 'code':19,84,114,194,419,1671,1737,1802,1987,1997,2002,2048,2383,2529 'codebas':1574 'collect':868,888,1078,2124,2465,2513 'combin':1075 'come':570 'common':479 'compos':2564 'confid':2505 'console.error':213,228,244,1369,1384 'console.log':391,397 'const':162,184,294,316,372,378,406,429,466,502,516,533,545,698,705,712,729,774,830,837,852,904,1002,1012,1031,1052,1081,1142,1152,1164,1189,1259,1288,1305,1333,1419,1492,1535,1553,1612,1620,1629,1634,1652,1672,1676,1700,1725,1773,1788,1803,1821,1835,1859,1993,2005,2049,2129,2142,2172,2179,2257,2282,2290,2330 'contract':98,132 'control':2561 'convers':1611 'convert':417,541,1570,2390,2527 'core':48 'correct':360 'creat':370,996,2365,2372 'createapierror':1994,2016,2027,2032 'createord':657,713,761,809 'creation':664 'criteria':2611 'data':53,1373,1445,1447,1461,1464,1656,1663 'db':827,1738 'db.find':164,296 'dberror':825,842,858,1730 'def':2197,2211,2215 'default':530,1568,1832,2072,2447 'defaultus':1565,1843 'delaym':1502,1515,1523 'deleteus':2331,2338 'describ':2579 'didn':2355 'differ':812,2427 'direct':1610 'display':1186 'domain':93 'doubl':503 'e':212,227,243,262,336,345,364,439,440,443,447,470,471,474,478,485,522,524,613,618,636,641,661,666,679,957,1198,1222,1230,1273,1274,1277,1281,1363,1374,1383,1389,1494,1498,1505,1596,1664,1665,1713,1718,1736,1741,1767,1805,1809,2099,2341 'e.bind':785,795,1868,1887,1901,1920 'e.chain':747,759,806,2413,2419 'e.chainw':863,2423,2432 'e.do':784,1867 'e.either':285,433,702,709,718,735,780,834,841,856,1016,1035,1057,1168,1658,1790,1863,2138 'e.field':1199 'e.filterorelse':741,752,789,799,2450,2456 'e.fold':410,1814,2434,2440 'e.fromnullable':547,1622,2388,2392 'e.fromoption':1638 'e.getapplicativevalidation':1004,1154 'e.getorelse':538,2443,2448 'e.isleft':2166 'e.isright':389 'e.left':291,301,380,519,536,1020,1025,1039,1045,1062,1068,1172,1177,1881,1895,1911,1934,2187,2370,2375 'e.map':507,1942,2395,2401 'e.mapleft':521,2150,2405,2411 'e.message':2342 'e.right':306,374,505,1029,1050,1073,1182,1875,1893,1909,1930,2191,2363,2368 'e.tooption':2160 'e.trycatch':436,1661,2378,2384 'e.trycatchk':468 'easi':1184 'edg':2532 'either':12,86,322,334,335,342,421,480,544,675,695,1009,1217,1229,1287,1311,1592,1646,1680,1784,1789,2473 'either/taskeither':2498 'eitherus':1635 'els':396,912,923,936 'email':910,917,1014,1019,1022,1028,1030,1087,1098,1104,1113,1120,1166,1171,1174,1179,1183 'email.includes':1024,1176 'empti':1918 'end':191,2537 'environ':2591 'environment-specif':2590 'equival':2471,2477 'error':3,8,21,34,50,94,97,108,159,170,256,325,344,351,400,411,413,434,442,445,473,476,515,523,526,532,696,725,813,850,870,875,889,894,902,905,948,952,985,994,1001,1010,1017,1036,1058,1080,1129,1157,1191,1264,1276,1279,1293,1312,1318,1342,1457,1460,1510,1526,1705,1715,1739,1776,1791,1799,1815,1817,1834,1982,2018,2040,2047,2058,2120,2151,2153,2204,2212,2255,2277,2284,2320,2326,2328,2371,2389,2408,2428,2455,2467,2494,2510,2515,2555,2565 'error-handl':107 'error.code':2060 'error.message':2076 'errors.find':1197 'errors.length':946 'errors.push':909,915,920,927,933,940 'escap':1745 'everywher':6,118,200,597 'exampl':886,2362 'except':82,122,123 'exception-heavi':81 'execut':1237,1302,1448 'exist':555 'expert':2596 'explod':189 'exponenti':1533 'extern':1697,1754 'extract':2445 'fail':183,214,229,246,280,412,537,566,576,594,617,640,665,1211,1366,1370,1385,1466,1717,2087,2251,2280,2310,2317,2323 'failur':379,2116,2127,2161,2201,2374 'fall':1549 'fallback':1542,2490 'fetch':616,639,1267,1430,1708,1716,2014 'fetchfromapi':1561 'fetchfromcach':1558 'fetchfromdb':838,865 'fetchjson':1701 'fetchnotif':1358,1439 'fetchpost':1300,1356,1436 'fetchset':1360,1442 'fetchus':1260,1296,1336,1424,1538,1778,1825,1839 'fetchwitherrorhandl':2006,2054 'fetchwithretri':1536 'field':1127,1135,1144,1150,1160,1193,1200 'field-level':1126 'fielderror':1134,1143,1173,1178 'filter':2297,2312 'first':724,879,1548,2509 'flow':2562 'fn':2379,2396,2406,2414,2424 'fold':405,2539 'forc':311 'forgotten':2558 'form':881,898,1083,1131,2468,2522 'form.age':932,938,1095 'form.email':908,1089 'form.email.includes':914 'form.password':919,1092 'form.password.length':925 'format':1181 'formdata':899,1084 'formerror':1140,1148,1169,1192 'formvalid':1153 'found':173,304,549,558,1345,1625,1641,2038,2063,2377 'fp':2,25,105,265,272,367,488,495,682,689,960,968,975,982,1245,1252,1402,1409,1416,1474,1482,1489,1599,1607,1693,1762,1770,2094,2102,2109,2229,2237,2244 'fp-error':1 'fp-ts':24,104,264,271,366,487,494,681,688,959,967,974,981,1244,1251,1401,1408,1415,1473,1481,1488,1598,1606,1692,1761,1769,2093,2101,2108,2228,2236,2243 'fromnul':546,550 'full':1981 'function':134,138,141,150,201,281,426,460,598,693,896,1225,1328,1577,1644,1699 'get':216,231,1782,2347 'geta':2418 'getb':2421 'getfielderror':1190 'getord':209 'getproduct':633,706,750,797 'getus':142,151,186,224,282,318,610,699,739,787 'getuserdata':1554 'getuserfromdb':1726 'getuserpost':1289,1308 'got':415 'guidanc':110 'handl':7,22,33,109,313,1319,1450,1983,2437,2541,2566 'handleuserfetch':2050 'hatch':1746 'heavi':83 'hell':588 'hold':341 'hope':62 'http':2033,2039 'id':143,152,156,160,165,187,283,289,292,297,319,700,707,832,839,854,862,1261,1269,1555,1559,1562,1617,1727,1735,1853,1888,1896,1943,1946,1951,1959,1967,1973,2253,2259,2262,2270,2272,2275,2283,2318,2325,2327,2336,2339 'idea':49,180 'import':260,268,362,483,491,677,685,955,963,971,978,1240,1248,1397,1405,1412,1469,1477,1485,1594,1602,1688,1757,1765,2089,2097,2105,2224,2232,2240 'index':2146,2154,2208,2216 'input':1848,1882,2174,2177,2178,2182,2190,2605 'instanceof':441,472,1275 'instead':54,2499 'interfac':1133,1851,1985,2112,2247 'intermedi':772 'invalid':916,1027,1180,2028,2188,2205,2213 'invis':125 'involv':91 'isnan':2185 'item':2085,2118,2133,2136,2145,2149,2152,2202,2210 'items.map':2144 'jargon':40 'json':431,438,456,2029 'json.parse':437,469,2385 'know':277,1158,1368 'last':1566 'lazi':1232 'least':991 'left':350,382,457,525,556,1103,1829,1972 'legaci':1801 'let':205,220,235,606,629,653,1346 'level':1128 'librari':1724 'like':195,1216,1286 'limit':2567 'list':2082 'll':462 'load':1372,1387 'loaddashboard':1329,1420,1454 'logerror':614,623,637,647,662 'longpassword':1116,1123 'manual':890,893 'map':2307,2324 'match':403,2045,2534,2576 'maybeus':1630,1637 'maybevalu':2394 'messag':407,822,828,1137,1146,1151,1201,1989,1995,2003 'messi':198,590,892,1321 'might':188,565,2086 'miss':2393,2613 'mix':1323 'multipl':869 'must':941,1070,1109,1883,1897,1913,1936,1974,2460 'mytaskeith':1774,1794 'n':508,509,2180,2186,2192 'name':1855,1902,1912,1944,1947,1953,1961,1969 'nea':965 'nea.getsemigroup':1005,1155 'nea.nonemptyarray':995,1141 'nea.of':1021,1026,1040,1046,1063,1069,1149 'need':78,1749,2517 'nest':586,595 'network':2017,2019,2068 'never':328 'new':158,169,444,475,1278,1341,1714 'non':1917 'non-empti':1916 'nonemptyarray':988 'notat':767 'noth':1233 'notif':1348,1352,1380,1438 'null':219,234,248,605,620,628,643,652,668,1376,1391,1874,1971 'null/undefined':560 'nullabl':542,1578,1590,2391 'number':1055,1059,1501,1503,1854,1892,1900,1977,1992,2000,2189,2206,2214 'o':1604 'o.fromnullable':1632 'o.none':2170 'o.option':1631 'o.some':2168 'obj':1869,1889,1903,1922 'obj.id':1891,1894 'obj.name':1905,1910 'obj.name.length':1907 'obj.tags':1924,1931 'obj.tags.every':1925 'object':1872,1886 'one':880,992,1365 'onerror':2380,2435,2444 'onfals':2452 'onsuccess':2436 'oop':381,383 'oper':481,563,1204,1208,1258,1284,1467,2219,2344,2416,2474 'option':1628,1780,1795,1830 'order':206,208,217,250,654,656,663,670,720,737,782 'order.total':241 'order.userid':225 'orderid':203,210 'output':2585 'parallel':1429 'pars':1846,2030 'parsedinput':1852,1865 'parseinput':1860,1950,1966 'parseint':2181 'parsejson':430,448,454 'parsenumb':2173,2193 'parseus':1673,1678 'partial':2221 'password':921,928,1033,1038,1041,1047,1051,1090,1099,1106,1115,1122 'password.length':1043 'pattern':43,333,402,1572,2044,2359 'payment':236,238,245,252 'payoff':2551 'permiss':2606 'pipe':269,408,492,504,518,535,686,738,749,783,860,979,1249,1295,1413,1423,1428,1453,1486,1507,1513,1557,1636,1838,1866,2012,2053,2106,2147,2157,2162,2241,2269,2273,2337,2399,2409,2417,2430 'pipelin':584,1395 'plain':1751 'posit':2462 'post':1294,1313,1347,1351,1379,1435 'power':569 'practic':20,42 'pragmat':103 'pred':2451 'predict':18 'preserv':1785 'prisma':1721 'prisma.user.finduniqueorthrow':1733 'problem':46,120 'process':853,2081,2122,2135,2148,2261,2274 'processallcollecterror':2130,2176 'processord':202 'processresult':2113,2141 'processuserord':599,730,775 'product':630,632,638,659,711,716,717,753,760,763,796,800,808,811 'product.stock':645,754,801 'productid':602,634,733,751,778,798 'promis':140,1220,1228,1315,1325,1581,1684,1744,1752,1811,2026 'promise.all':1355 'promise.reject':1816 'promise.resolve':1819 'provid':528 'quick':2357 'r':1271,1711,2132,2140,2165,2167,2298,2299,2308,2313,2314 'r.json':1272,1712 'r.left':2169 'r.result':2309 'r.type':2305,2322 'raw':1861,1871,1873,1876 'rawdata':1679 're':2546 'readi':1304,2547 'real':45,112,568,1573,1844 'record':1878 'refer':2358 'renderdashboard':1463 'rendererror':1459 'replac':80 'report':2349 'requir':161,293,911,922,935,1023,1042,1065,1105,1175,2604 'resort':1567 'respons':2021 'response.json':2024 'response.ok':2022 'response.status':2034,2035,2041 'result':317,320,332,390,409,604,1306,1621,1677,2143,2158,2163,2285,2291,2294,2296,2303,2311,2400,2410 'result.left':398 'result.right':392 'retri':1465,1493,1519,1527,1537 'return':66,174,218,233,247,249,255,290,300,305,619,627,642,651,667,669,694,944,1008,1227,1310,1375,1377,1390,2064,2069,2073,2155,2184,2493 'reus':463 'review':2597 'right':353,357,376,451,511,552,1119,1958 'run':1234,2345 'safe':1849 'safepars':1653,1674 'safeparsejson':467 'safeti':2607 'save':2433 'scenario':1845 'schema':1654 'schema.parse':1662 'scope':2578 'separ':2128 'sequenc':972,1085,1406,1432,1433,2463 'set':1349,1353,1381,1441 'short':930,1049,1108 'showdata':2442 'showerror':2441 'showgenericerror':2075 'shownotfoundpag':2066 'showofflinemessag':2071 'showuserprofil':2079 'signatur':139 'simpl':339 'skill':28,2570 'skill-fp-errors' 'solut':254 'someon':63 'sometim':871,1747 'source-sickn33' 'spaghetti':37 'specif':2592 'status':1991,1999,2004 'step':574,592 'stock':650,758,805 'stop':4,116,726,2507,2598 'string':144,153,204,284,286,323,432,446,477,601,603,701,703,708,710,719,732,734,736,777,779,781,823,829,833,836,840,855,903,906,1015,1018,1034,1037,1136,1138,1145,1147,1167,1170,1194,1195,1262,1280,1291,1331,1422,1556,1703,1728,1856,1858,1864,1879,1906,1919,1929,1933,1941,1988,1990,1996,2008,2052,2121,2139,2175,2254,2256,2260,2263,2265,2319,2321,2333 'substitut':2588 'succeed':2249,2288,2295,2302,2306 'success':327,354,373,394,500,2114,2125,2156,2198,2222,2367,2398,2610 'summari':2491 'switch':2059 'syntaxerror':458 't.chain':1518 't.delay':1514 't.map':2293 't.of':1458,1462,1516,1842,2065,2070,2074,2078,2278,2286 't.sequencearray':2292 't.task':2267 'tag':1857,1921,1935,1945,1948,1955,1963,1970 'task':90,1496,1508,1520,2574 'taskeith':14,88,1205,1213,1221,1394,1686,2470,2476 'te':1242,1399,1479,1690,1759,1807,1812,2226 'te.applypar':1434 'te.chain':1298,1426,2020,2483 'te.chainw':2484 'te.filterorelse':2487 'te.fold':1456,2057,2276,2485 'te.getorelse':1841,2486 'te.left':1525,2031,2479 'te.map':1444,2481 'te.mapleft':2340,2482 'te.orelse':1509,1560,1563,2488 'te.right':1564,2478 'te.taskeither':1263,1292,1497,1504,1704,1729,1775,1808,2009,2264 'te.trycatch':1266,1707,1732,2013,2023,2480 'teach':29 'test':1954,1962,2594 'think':356 'throughout':1669 'throw':5,56,117,157,168,418,425,1340,1576,1643,1797,1827,2382,2501 'throwing/promise':2528 'time':1531 'toerror':2387 'togeth':723 'topic-agent-skills' 'topic-agentic-skills' 'topic-ai-agent-skills' 'topic-ai-agents' 'topic-ai-coding' 'topic-ai-workflows' 'topic-antigravity' 'topic-antigravity-skills' 'topic-claude-code' 'topic-claude-code-skills' 'topic-codex-cli' 'topic-codex-skills' 'tothrowingpromis':1804,1824 'track':73,2553 'transform':498,513,2397,2407 'treat':2583 'tri':207,222,237,608,631,655,1332,1350,1546 'true':450,453 'try/catch':36,199,587,596,1322,2559 'trycatch':428 'trycatchk':465 'ts':26,106,266,273,368,489,496,683,690,961,969,976,983,1246,1253,1403,1410,1417,1475,1483,1490,1600,1608,1694,1763,1771,2095,2103,2110,2230,2238,2245 'type':128,814,818,820,824,826,851,993,1139,2279,2287,2301,2316,2429 'typeof':1870,1890,1904,1927 'typescript':71,100,135,197,259,276,361,422,482,589,676,817,891,954,1132,1239,1320,1396,1468,1545,1593,1647,1687,1756,1850,1984,2088,2223,2552 'u':1615 'u.id':1616 'ui':1188 'undefin':1056,1061,1196,1517,1619 'union':848 'unknown':435,1657,1862,1880,1998,2119 'url':1702,1709,2007,2015 'usag':1096,1949,2042,2171,2329 'use':11,76,464,765,815,1212,1668,2360,2497,2518,2538,2568 'user':145,154,163,167,171,175,185,221,223,232,251,287,295,299,302,307,324,551,553,607,609,615,624,658,704,714,715,742,744,748,762,786,790,792,807,810,843,859,1265,1299,1334,1339,1343,1378,1388,1427,1446,1613,1618,1623,1626,1633,1639,1682,1731,1777,1792,1822,1836,1847,2077,2080 'user.cardid':240 'user.id':1301,1357,1359,1361,1437,1440,1443 'user.isactive':622,743,791 'userid':600,611,731,740,776,788,1290,1297,1330,1337,1421,1425,2051,2056,2332,2335 'users.find':1614 'userschema':1675 'valid':92,449,452,821,882,900,945,950,1003,1006,1077,1086,2431,2453,2464,2469,2519,2593 'validateag':1053,1094 'validateemail':1013,1088,1165 'validateform':897,1082,1097,1112 'validateinput':831,861 'validatepassword':1032,1091 'validationerror':819,835,857 'validid':864,866 'valu':10,69,258,348,371,395,414,416,501,534,773,1579,1818,1820,2364,2496 'void':60 'w':845 'want':102,873 'wider':846 'without':35 'work':1586,2352 'wrap':423,1255,1648,1696,1719,2381,2524 'write':579 'wrong':1968 'x':2402,2403,2457,2458 'zoderror':1659,1667,1681 'zodschema':1655","prices":[{"id":"f68989ab-8fee-4fd8-a1bf-89a15606b7cf","listingId":"eebb21fa-7080-4d0c-bf96-b8931ea17392","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"sickn33","category":"antigravity-awesome-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T21:37:26.015Z"}],"sources":[{"listingId":"eebb21fa-7080-4d0c-bf96-b8931ea17392","source":"github","sourceId":"sickn33/antigravity-awesome-skills/fp-errors","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-errors","isPrimary":false,"firstSeenAt":"2026-04-18T21:37:26.015Z","lastSeenAt":"2026-04-24T00:50:58.031Z"}],"details":{"listingId":"eebb21fa-7080-4d0c-bf96-b8931ea17392","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"fp-errors","github":{"repo":"sickn33/antigravity-awesome-skills","stars":34793,"topics":["agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity","antigravity-skills","claude-code","claude-code-skills","codex-cli","codex-skills","cursor","cursor-skills","developer-tools","gemini-cli","gemini-skills","kiro","mcp","skill-library"],"license":"mit","html_url":"https://github.com/sickn33/antigravity-awesome-skills","pushed_at":"2026-04-24T00:28:59Z","description":"Installable GitHub library of 1,400+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.","skill_md_sha":"f43754c9d2143582023b45f013c4af134da14c88","skill_md_path":"skills/fp-errors/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-errors"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"fp-errors","description":"Stop throwing everywhere - handle errors as values using Either and TaskEither for cleaner, more predictable code"},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-errors"},"updatedAt":"2026-04-24T00:50:58.031Z"}}