[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-468085e5-e084-4322-8343-e3cc0f772f1b":3,"$fYCMpKIGTChAcWNrqVXRW5-O03BucCZi3-sjB77AP4tk":42},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":33},"468085e5-e084-4322-8343-e3cc0f772f1b","fp-errors","停止到处抛出异常 - 使用 Either 和 TaskEither 处理错误作为值，以获得更干净、更可预测的代码","cat_prod_project","mod_productivity","sickn33,productivity","---\nname: fp-errors\ndescription: Stop throwing everywhere - handle errors as values using Either and TaskEither for cleaner, more predictable code\nrisk: unknown\nsource: community\nversion: 1.0.0\nauthor: kadu\ntags:\n  - fp-ts\n  - error-handling\n  - either\n  - task-either\n  - typescript\n  - validation\n  - practical\n---\n\n# Practical Error Handling with fp-ts\n\nThis skill teaches you how to handle errors without try\u002Fcatch 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\u002F\u002F What this function signature promises:\nfunction getUser(id: string): User\n\n\u002F\u002F 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\u002F\u002F The caller has no idea this can fail\nconst user = getUser(id) \u002F\u002F Might explode!\n```\n\nYou end up with code like this:\n\n```typescript\n\u002F\u002F MESSY: try\u002Fcatch 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\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Now TypeScript KNOWS this can fail\nfunction getUser(id: string): E.Either\u003Cstring, 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\u002F\u002F The caller is forced to handle both cases\nconst result = getUser(id)\n\u002F\u002F result is Either\u003Cstring, User> - error OR success, never both\n```\n\n---\n\n## 2. The Result Pattern (Either)\n\n`Either\u003CE, 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\u002FEither'\n\n\u002F\u002F Creating values\nconst success = E.right(42)           \u002F\u002F Right(42)\nconst failure = E.left('Oops')        \u002F\u002F Left('Oops')\n\n\u002F\u002F Checking what you have\nif (E.isRight(result)) {\n  console.log(result.right) \u002F\u002F The success value\n} else {\n  console.log(result.left)  \u002F\u002F The error\n}\n\n\u002F\u002F 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\u002F\u002F Wrap any throwing function with tryCatch\nconst parseJSON = (json: string): E.Either\u003CError, unknown> =>\n  E.tryCatch(\n    () => JSON.parse(json),\n    (e) => (e instanceof Error ? e : new Error(String(e)))\n  )\n\nparseJSON('{\"valid\": true}')  \u002F\u002F Right({ valid: true })\nparseJSON('not json')          \u002F\u002F Left(SyntaxError: ...)\n\n\u002F\u002F 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\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Transform the success value\nconst doubled = pipe(\n  E.right(21),\n  E.map(n => n * 2)\n) \u002F\u002F Right(42)\n\n\u002F\u002F Transform the error\nconst betterError = pipe(\n  E.left('bad'),\n  E.mapLeft(e => `Error: ${e}`)\n) \u002F\u002F Left('Error: bad')\n\n\u002F\u002F Provide a default for errors\nconst value = pipe(\n  E.left('failed'),\n  E.getOrElse(() => 0)\n) \u002F\u002F 0\n\n\u002F\u002F Convert nullable to Either\nconst fromNullable = E.fromNullable('not found')\nfromNullable(user)  \u002F\u002F Right(user) if exists, Left('not found') if null\u002Fundefined\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\u002FCatch Hell\n\n```typescript\n\u002F\u002F MESSY: Each step can fail, nested try\u002Fcatch 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 \u003C 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\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Each function returns Either\u003CError, T>\nconst getUser = (id: string): E.Either\u003Cstring, User> => { ... }\nconst getProduct = (id: string): E.Either\u003Cstring, Product> => { ... }\nconst createOrder = (user: User, product: Product): E.Either\u003Cstring, Order> => { ... }\n\n\u002F\u002F Chain them together - first error stops the chain\nconst processUserOrder = (userId: string, productId: string): E.Either\u003Cstring, 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\u002F\u002F Or use Do notation for cleaner access to intermediate values\nconst processUserOrder = (userId: string, productId: string): E.Either\u003Cstring, 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\u003CValidationError, string> => { ... }\nconst fetchFromDb = (id: string): E.Either\u003CDbError, User> => { ... }\n\n\u002F\u002F chainW (W = \"wider\") automatically unions the error types\nconst process = (id: string): E.Either\u003CValidationError | 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\u002F\u002F 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 \u003C 8) {\n    errors.push('Password too short')\n  }\n\n  if (!form.age) {\n    errors.push('Age required')\n  } else if (form.age \u003C 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\u002FEither'\nimport * as NEA from 'fp-ts\u002FNonEmptyArray'\nimport { sequenceS } from 'fp-ts\u002FApply'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Errors as a NonEmptyArray (always at least one)\ntype Errors = NEA.NonEmptyArray\u003Cstring>\n\n\u002F\u002F Create the applicative that accumulates errors\nconst validation = E.getApplicativeValidation(NEA.getSemigroup\u003Cstring>())\n\n\u002F\u002F Validators that return Either\u003CErrors, T>\nconst validateEmail = (email: string): E.Either\u003CErrors, 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\u003CErrors, string> =>\n  !password ? E.left(NEA.of('Password required'))\n  : password.length \u003C 8 ? E.left(NEA.of('Password too short'))\n  : E.right(password)\n\nconst validateAge = (age: number | undefined): E.Either\u003CErrors, number> =>\n  age === undefined ? E.left(NEA.of('Age required'))\n  : age \u003C 18 ? E.left(NEA.of('Must be 18+'))\n  : E.right(age)\n\n\u002F\u002F 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\u002F\u002F Usage\nvalidateForm({ email: '', password: '123', age: 15 })\n\u002F\u002F Left(['Email required', 'Password too short', 'Must be 18+'])\n\nvalidateForm({ email: 'a@b.com', password: 'longpassword', age: 25 })\n\u002F\u002F 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\u003CFieldError>\n\nconst fieldError = (field: string, message: string): FormErrors =>\n  NEA.of({ field, message })\n\nconst formValidation = E.getApplicativeValidation(NEA.getSemigroup\u003CFieldError>())\n\n\u002F\u002F Now errors know which field they belong to\nconst validateEmail = (email: string): E.Either\u003CFormErrors, string> =>\n  !email ? E.left(fieldError('email', 'Required'))\n  : !email.includes('@') ? E.left(fieldError('email', 'Invalid format'))\n  : E.right(email)\n\n\u002F\u002F 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\u003CE, A>` = a function that returns `Promise\u003CEither\u003CE, A>>`\n- Lazy: nothing runs until you execute it\n\n```typescript\nimport * as TE from 'fp-ts\u002FTaskEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Wrap any async operation\nconst fetchUser = (id: string): TE.TaskEither\u003CError, User> =>\n  TE.tryCatch(\n    () => fetch(`\u002Fapi\u002Fusers\u002F${id}`).then(r => r.json()),\n    (e) => (e instanceof Error ? e : new Error(String(e)))\n  )\n\n\u002F\u002F Chain async operations - just like Either\nconst getUserPosts = (userId: string): TE.TaskEither\u003CError, Post[]> =>\n  pipe(\n    fetchUser(userId),\n    TE.chain(user => fetchPosts(user.id))\n  )\n\n\u002F\u002F Execute when ready\nconst result = await getUserPosts('123')() \u002F\u002F Returns Either\u003CError, Post[]>\n```\n\n### Before: Promise Chain with Error Handling\n\n```typescript\n\u002F\u002F MESSY: try\u002Fcatch 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      \u002F\u002F 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\u002FTaskEither'\nimport { sequenceS } from 'fp-ts\u002FApply'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\nconst loadDashboard = (userId: string) =>\n  pipe(\n    fetchUser(userId),\n    TE.chain(user =>\n      pipe(\n        \u002F\u002F 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\u002F\u002F 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\u002FTask'\nimport * as TE from 'fp-ts\u002FTaskEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\nconst retry = \u003CE, A>(\n  task: TE.TaskEither\u003CE, A>,\n  attempts: number,\n  delayMs: number\n): TE.TaskEither\u003CE, 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\u002F\u002F 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\u002F\u002F 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)) \u002F\u002F 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\u002FEither'\nimport * as O from 'fp-ts\u002FOption'\n\n\u002F\u002F Direct conversion\nconst user = users.find(u => u.id === id) \u002F\u002F User | undefined\nconst result = E.fromNullable('User not found')(user)\n\n\u002F\u002F From Option\nconst maybeUser: O.Option\u003CUser> = 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\u002F\u002F Wrap at the boundary\nconst safeParse = \u003CT>(schema: ZodSchema\u003CT>) => (data: unknown): E.Either\u003CZodError, T> =>\n  E.tryCatch(\n    () => schema.parse(data),\n    (e) => e as ZodError\n  )\n\n\u002F\u002F Use throughout your code\nconst parseUser = safeParse(UserSchema)\nconst result = parseUser(rawData) \u002F\u002F Either\u003CZodError, User>\n```\n\n### From Promise to TaskEither\n\n```typescript\nimport * as TE from 'fp-ts\u002FTaskEither'\n\n\u002F\u002F Wrap external async functions\nconst fetchJson = \u003CT>(url: string): TE.TaskEither\u003CError, T> =>\n  TE.tryCatch(\n    () => fetch(url).then(r => r.json()),\n    (e) => new Error(`Fetch failed: ${e}`)\n  )\n\n\u002F\u002F Wrap axios, prisma, any async library\nconst getUserFromDb = (id: string): TE.TaskEither\u003CDbError, 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\u002FTaskEither'\nimport * as E from 'fp-ts\u002FEither'\n\nconst myTaskEither: TE.TaskEither\u003CError, User> = fetchUser('123')\n\n\u002F\u002F Option 1: Get the Either (preserves both cases)\nconst either: E.Either\u003CError, User> = await myTaskEither()\n\n\u002F\u002F Option 2: Throw on error (for legacy code)\nconst toThrowingPromise = \u003CE, A>(te: TE.TaskEither\u003CE, A>): Promise\u003CA> =>\n  te().then(E.fold(\n    (error) => Promise.reject(error),\n    (value) => Promise.resolve(value)\n  ))\n\nconst user = await toThrowingPromise(fetchUser('123')) \u002F\u002F Throws if Left\n\n\u002F\u002F 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\u003Cstring, ParsedInput> =>\n  pipe(\n    E.Do,\n    E.bind('obj', () =>\n      typeof raw === 'object' && raw !== null\n        ? E.right(raw as Record\u003Cstring, 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\u002F\u002F Usage\nparseInput({ id: 1, name: 'test', tags: ['a', 'b'] })\n\u002F\u002F Right({ id: 1, name: 'test', tags: ['a', 'b'] })\n\nparseInput({ id: 'wrong', name: '', tags: null })\n\u002F\u002F 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 = \u003CT>(url: string): TE.TaskEither\u003CApiError, 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\u003CT>,\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\u002F\u002F Usage with pattern matching on error codes\nconst handleUserFetch = (userId: string) =>\n  pipe(\n    fetchWithErrorHandling\u003CUser>(`\u002Fapi\u002Fusers\u002F${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\u002FArray'\nimport * as E from 'fp-ts\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\ninterface ProcessResult\u003CT> {\n  successes: T[]\n  failures: Array\u003C{ item: unknown; error: string }>\n}\n\n\u002F\u002F Process all, collect successes and failures separately\nconst processAllCollectErrors = \u003CT, R>(\n  items: T[],\n  process: (item: T) => E.Either\u003Cstring, R>\n): ProcessResult\u003CR> => {\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\u002F\u002F 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\u002F\u002F {\n\u002F\u002F   successes: [1, 3],\n\u002F\u002F   failures: [\n\u002F\u002F     { item: 'abc', error: 'Invalid number: abc', index: 1 },\n\u002F\u002F     { item: 'def', error: 'Invalid number: def', index: 3 }\n\u002F\u002F   ]\n\u002F\u002F }\n```\n\n### Bulk Operations with Partial Success\n\n```typescript\nimport * as TE from 'fp-ts\u002FTaskEither'\nimport * as T from 'fp-ts\u002FTask'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\ninterface BulkResult\u003CT> {\n  succeeded: T[]\n  failed: Array\u003C{ id: string; error: string }>\n}\n\nconst bulkProcess = \u003CT>(\n  ids: string[],\n  process: (id: string) => TE.TaskEither\u003Cstring, T>\n): T.Task\u003CBulkResult\u003CT>> =>\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\u002F\u002F Usage\nconst deleteUsers = (userIds: string[]) =>\n  bulkProcess(userIds, id =>\n    pipe(\n      deleteUser(id),\n      TE.mapLeft(e => e.message)\n    )\n  )\n\n\u002F\u002F 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\u002FTaskEither 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\u002FPromise 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\u002Fcatch, 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.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,123,785,"2026-05-16 13:18:38",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"效率工具","productivity","mdi-lightning-bolt-outline","文档处理、数据分析、自动化工作流",4,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":25,"skillCount":32,"createdAt":26},"项目管理","project-management","mdi-view-dashboard-outline","任务管理、进度追踪、报告生成",13,[34],{"id":35,"skillId":4,"version":36,"fileName":37,"fileSize":38,"filePath":39,"fileHash":40,"manifest":41,"createdAt":19},"c207f554-9829-459a-9c5f-58cf1b54f300","1.0.0","fp-errors.zip",7194,"uploads\u002Fskills\u002F468085e5-e084-4322-8343-e3cc0f772f1b\u002Ffp-errors.zip","1a9396ec7d787f04be02b8271ae57eee62739b97394bf53337280a19542f4ca3","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":22782}]",{"code":43,"message":44,"data":45},200,"success",{"items":46,"stats":47,"page":50},[],{"averageRating":48,"totalRatings":48,"ratingCounts":49},0,[48,48,48,48,48],{"limit":51,"offset":48,"hasMore":52,"nextOffset":51,"ratedOnly":16},15,false]