[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-1e1c4a7d-a4af-410c-96ca-834e62778ac9":3,"$fpGzQyPI-uYFpJTl1jvsIelaIBdHymK_VtyXLDT9EUhY":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},"1e1c4a7d-a4af-410c-96ca-834e62778ac9","fp-async","使用TaskEither的实用异步模式 - 清洁的管道而非try\u002Fcatch地狱，附带真实API示例","cat_coding_backend","mod_coding","sickn33,coding","---\nname: fp-async\ndescription: Practical async patterns using TaskEither - clean pipelines instead of try\u002Fcatch hell, with real API examples\nrisk: unknown\nsource: community\nversion: 1.0.0\nauthor: kadu\ntags:\n  - fp-ts\n  - typescript\n  - async\n  - error-handling\n  - practical\n  - promises\n  - api\n  - fetch\n---\n\n# Practical Async Patterns with fp-ts\n\nStop writing nested try\u002Fcatch blocks. Stop losing error context. Start building clean async pipelines that handle errors properly.\n\n**TaskEither is simply an async operation that tracks success or failure.** That's it. No fancy terminology needed.\n\n## When to Use\n- You need async error handling in TypeScript with `TaskEither`.\n- The task involves wrapping Promises, composing API calls, or replacing nested `try\u002Fcatch` flows.\n- You want practical fp-ts async patterns instead of academic explanations.\n\n```typescript\n\u002F\u002F TaskEither\u003CError, User> means:\n\u002F\u002F \"An async operation that either fails with Error or succeeds with User\"\n```\n\n---\n\n## 1. Wrapping Promises Safely\n\n### The Problem: Try\u002FCatch Everywhere\n\n```typescript\n\u002F\u002F BEFORE: Try\u002Fcatch hell\nasync function getUserData(userId: string) {\n  try {\n    const response = await fetch(`\u002Fapi\u002Fusers\u002F${userId}`)\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}`)\n    }\n    const user = await response.json()\n\n    try {\n      const posts = await fetch(`\u002Fapi\u002Fusers\u002F${userId}\u002Fposts`)\n      if (!posts.ok) {\n        throw new Error(`HTTP ${posts.status}`)\n      }\n      const postsData = await posts.json()\n      return { user, posts: postsData }\n    } catch (postsError) {\n      \u002F\u002F Now what? Return partial data? Rethrow? Log?\n      console.error('Failed to fetch posts:', postsError)\n      return { user, posts: [] }\n    }\n  } catch (error) {\n    \u002F\u002F Lost all context about what failed\n    console.error('Something failed:', error)\n    throw error\n  }\n}\n```\n\n### The Solution: Wrap Once, Handle Cleanly\n\n```typescript\nimport * as TE from 'fp-ts\u002FTaskEither'\nimport * as E from 'fp-ts\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F One wrapper function - reuse everywhere\nconst fetchJson = \u003CT>(url: string): TE.TaskEither\u003CError, T> =>\n  TE.tryCatch(\n    async () => {\n      const response = await fetch(url)\n      if (!response.ok) {\n        throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n      }\n      return response.json()\n    },\n    (error) => error instanceof Error ? error : new Error(String(error))\n  )\n\n\u002F\u002F AFTER: Clean and composable\nconst getUser = (userId: string) => fetchJson\u003CUser>(`\u002Fapi\u002Fusers\u002F${userId}`)\nconst getPosts = (userId: string) => fetchJson\u003CPost[]>(`\u002Fapi\u002Fusers\u002F${userId}\u002Fposts`)\n```\n\n### tryCatch Explained\n\n`TE.tryCatch` takes two things:\n1. An async function that might throw\n2. A function to convert the thrown value into your error type\n\n```typescript\nTE.tryCatch(\n  () => somePromise,           \u002F\u002F The async work\n  (thrown) => toError(thrown)  \u002F\u002F Convert failures to your error type\n)\n```\n\n### Creating Success and Failure Values\n\n```typescript\n\u002F\u002F Wrap a value as success\nconst success = TE.right\u003CError, number>(42)\n\n\u002F\u002F Wrap a value as failure\nconst failure = TE.left\u003CError, number>(new Error('Nope'))\n\n\u002F\u002F From a nullable value (null\u002Fundefined becomes error)\nconst fromNullable = TE.fromNullable(new Error('Value was null'))\nconst result = fromNullable(maybeUser) \u002F\u002F TaskEither\u003CError, User>\n\n\u002F\u002F From a condition\nconst mustBePositive = TE.fromPredicate(\n  (n: number) => n > 0,\n  (n) => new Error(`Expected positive, got ${n}`)\n)\n```\n\n---\n\n## 2. Chaining Async Operations\n\n### The Problem: Callback Hell \u002F Nested Awaits\n\n```typescript\n\u002F\u002F BEFORE: Deeply nested, hard to follow\nasync function processOrder(orderId: string) {\n  try {\n    const order = await fetchOrder(orderId)\n    if (!order) throw new Error('Order not found')\n\n    try {\n      const user = await fetchUser(order.userId)\n      if (!user) throw new Error('User not found')\n\n      try {\n        const inventory = await checkInventory(order.items)\n        if (!inventory.available) throw new Error('Out of stock')\n\n        try {\n          const payment = await chargePayment(user, order.total)\n          if (!payment.success) throw new Error('Payment failed')\n\n          try {\n            const shipment = await createShipment(order, user)\n            return { order, shipment, payment }\n          } catch (e) {\n            \u002F\u002F Refund payment? Log? What's the state now?\n            await refundPayment(payment.id)\n            throw e\n          }\n        } catch (e) {\n          throw e\n        }\n      } catch (e) {\n        throw e\n      }\n    } catch (e) {\n      throw e\n    }\n  } catch (e) {\n    console.error('Order processing failed', e)\n    throw e\n  }\n}\n```\n\n### The Solution: Clean Pipelines with chain\n\n```typescript\n\u002F\u002F AFTER: Flat, readable pipeline\nconst processOrder = (orderId: string) =>\n  pipe(\n    fetchOrder(orderId),\n    TE.chain(order => fetchUser(order.userId)),\n    TE.chain(user =>\n      pipe(\n        checkInventory(order.items),\n        TE.chain(inventory => chargePayment(user, order.total))\n      )\n    ),\n    TE.chain(payment => createShipment(order, user, payment))\n  )\n```\n\n### chain vs map\n\nUse `map` when your transformation is synchronous and can't fail:\n\n```typescript\npipe(\n  fetchUser(userId),\n  TE.map(user => user.name.toUpperCase())  \u002F\u002F Just transforms the value\n)\n```\n\nUse `chain` (or `flatMap`) when your transformation is async or can fail:\n\n```typescript\npipe(\n  fetchUser(userId),\n  TE.chain(user => fetchOrders(user.id))  \u002F\u002F Returns another TaskEither\n)\n```\n\n### Building Context with Do Notation\n\nWhen you need values from multiple steps:\n\n```typescript\n\u002F\u002F BEFORE: Have to thread values through manually\nconst processOrderManual = (orderId: string) =>\n  pipe(\n    fetchOrder(orderId),\n    TE.chain(order =>\n      pipe(\n        fetchUser(order.userId),\n        TE.chain(user =>\n          pipe(\n            chargePayment(user, order.total),\n            TE.map(payment => ({ order, user, payment }))\n          )\n        )\n      )\n    )\n  )\n\n\u002F\u002F AFTER: Do notation keeps everything accessible\nconst processOrder = (orderId: string) =>\n  pipe(\n    TE.Do,\n    TE.bind('order', () => fetchOrder(orderId)),\n    TE.bind('user', ({ order }) => fetchUser(order.userId)),\n    TE.bind('payment', ({ user, order }) => chargePayment(user, order.total)),\n    TE.bind('shipment', ({ order, user }) => createShipment(order, user)),\n    TE.map(({ order, payment, shipment }) => ({\n      orderId: order.id,\n      paymentId: payment.id,\n      trackingNumber: shipment.tracking\n    }))\n  )\n```\n\n---\n\n## 3. Parallel vs Sequential Execution\n\n### When to Use Each\n\n**Sequential** (one after another):\n- When each operation depends on the previous result\n- When you need to respect rate limits\n- When order matters\n\n**Parallel** (all at once):\n- When operations are independent\n- When you want speed\n- When fetching multiple resources by ID\n\n### Sequential Chaining\n\n```typescript\n\u002F\u002F Operations depend on each other - must be sequential\nconst getUserWithOrg = (userId: string) =>\n  pipe(\n    fetchUser(userId),                              \u002F\u002F First: get user\n    TE.chain(user => fetchTeam(user.teamId)),      \u002F\u002F Then: get their team\n    TE.chain(team => fetchOrganization(team.orgId)) \u002F\u002F Finally: get org\n  )\n```\n\n### Parallel Execution\n\n```typescript\nimport { sequenceT } from 'fp-ts\u002FApply'\n\n\u002F\u002F Independent operations - run in parallel\nconst getDashboardData = (userId: string) =>\n  sequenceT(TE.ApplyPar)(\n    fetchUser(userId),\n    fetchNotifications(userId),\n    fetchRecentActivity(userId)\n  ) \u002F\u002F Returns TaskEither\u003CError, [User, Notification[], Activity[]]>\n\n\u002F\u002F With destructuring:\nconst getDashboard = (userId: string) =>\n  pipe(\n    sequenceT(TE.ApplyPar)(\n      fetchUser(userId),\n      fetchNotifications(userId),\n      fetchRecentActivity(userId)\n    ),\n    TE.map(([user, notifications, activities]) => ({\n      user,\n      notifications,\n      activities,\n      unreadCount: notifications.filter(n => !n.read).length\n    }))\n  )\n```\n\n### Parallel Array Operations\n\n```typescript\n\u002F\u002F Fetch multiple users in parallel\nconst userIds = ['1', '2', '3', '4', '5']\n\n\u002F\u002F TE.traverseArray runs all fetches in parallel\nconst fetchAllUsers = pipe(\n  userIds,\n  TE.traverseArray(fetchUser)\n) \u002F\u002F TaskEither\u003CError, readonly User[]>\n\n\u002F\u002F Note: Fails fast - if ANY request fails, the whole thing fails\n\u002F\u002F All errors after the first are lost\n```\n\n### Parallel with Batch Control\n\nWhen you need to limit concurrent requests:\n\n```typescript\nconst chunk = \u003CT>(arr: T[], size: number): T[][] => {\n  const chunks: T[][] = []\n  for (let i = 0; i \u003C arr.length; i += size) {\n    chunks.push(arr.slice(i, i + size))\n  }\n  return chunks\n}\n\n\u002F\u002F Process in batches of 5 concurrent requests\nconst fetchUsersWithLimit = (userIds: string[]) => {\n  const batches = chunk(userIds, 5)\n\n  return pipe(\n    batches,\n    \u002F\u002F Process batches sequentially\n    TE.traverseArray(batch =>\n      \u002F\u002F But within each batch, run in parallel\n      pipe(batch, TE.traverseArray(fetchUser))\n    ),\n    TE.map(results => results.flat())\n  )\n}\n```\n\n### Sequential When Parallel Looks Tempting\n\n```typescript\n\u002F\u002F WRONG: This looks parallel but order might matter for DB operations\nconst createUserAndProfile = (userData: UserData) =>\n  sequenceT(TE.ApplyPar)(\n    createUser(userData),           \u002F\u002F Creates user with ID\n    createProfile(userData.profile) \u002F\u002F Needs user ID - race condition!\n  )\n\n\u002F\u002F RIGHT: Sequential when there's a dependency\nconst createUserAndProfile = (userData: UserData) =>\n  pipe(\n    createUser(userData),\n    TE.chain(user =>\n      pipe(\n        createProfile(user.id, userData.profile),\n        TE.map(profile => ({ user, profile }))\n      )\n    )\n  )\n```\n\n---\n\n## 4. Error Recovery Patterns\n\n### Fallback to Alternative\n\n```typescript\n\u002F\u002F Try primary API, fall back to cache\nconst getUserWithFallback = (userId: string) =>\n  pipe(\n    fetchUserFromApi(userId),\n    TE.orElse(() => fetchUserFromCache(userId))\n  )\n\n\u002F\u002F Chain multiple fallbacks\nconst getConfigRobust = () =>\n  pipe(\n    fetchRemoteConfig(),\n    TE.orElse(() => loadLocalConfig()),\n    TE.orElse(() => TE.right(defaultConfig))\n  )\n```\n\n### Conditional Recovery\n\n```typescript\n\u002F\u002F Only recover from specific errors\nconst fetchUserOrCreate = (userId: string) =>\n  pipe(\n    fetchUser(userId),\n    TE.orElse(error =>\n      error.message.includes('404') || error.message.includes('not found')\n        ? createDefaultUser(userId)\n        : TE.left(error)  \u002F\u002F Re-throw other errors\n    )\n  )\n```\n\n### Typed Error Recovery\n\n```typescript\ntype ApiError =\n  | { _tag: 'NotFound'; id: string }\n  | { _tag: 'NetworkError'; cause: Error }\n  | { _tag: 'Unauthorized' }\n\nconst fetchUser = (id: string): TE.TaskEither\u003CApiError, User> =>\n  TE.tryCatch(\n    async () => {\n      const res = await fetch(`\u002Fapi\u002Fusers\u002F${id}`)\n      if (res.status === 404) throw { _tag: 'NotFound', id }\n      if (res.status === 401) throw { _tag: 'Unauthorized' }\n      if (!res.ok) throw { _tag: 'NetworkError', cause: new Error(`HTTP ${res.status}`) }\n      return res.json()\n    },\n    (e): ApiError =>\n      typeof e === 'object' && e !== null && '_tag' in e\n        ? e as ApiError\n        : { _tag: 'NetworkError', cause: e instanceof Error ? e : new Error(String(e)) }\n  )\n\n\u002F\u002F Handle specific errors differently\nconst getUserOrGuest = (userId: string) =>\n  pipe(\n    fetchUser(userId),\n    TE.orElse(error => {\n      switch (error._tag) {\n        case 'NotFound':\n          return TE.right(createGuestUser())\n        case 'Unauthorized':\n          return TE.left(error) \u002F\u002F Propagate auth errors\n        case 'NetworkError':\n          return fetchUserFromCache(userId) \u002F\u002F Try cache on network issues\n      }\n    })\n  )\n```\n\n### Retry with Exponential Backoff\n\n```typescript\nimport * as T from 'fp-ts\u002FTask'\n\nconst wait = (ms: number): T.Task\u003Cvoid> =>\n  () => new Promise(resolve => setTimeout(resolve, ms))\n\nconst retry = \u003CE, A>(\n  operation: TE.TaskEither\u003CE, A>,\n  maxAttempts: number,\n  baseDelayMs: number = 1000\n): TE.TaskEither\u003CE, A> => {\n  const attempt = (remaining: number, delay: number): TE.TaskEither\u003CE, A> =>\n    pipe(\n      operation,\n      TE.orElse(error =>\n        remaining \u003C= 1\n          ? TE.left(error)\n          : pipe(\n              TE.fromTask(wait(delay)),\n              TE.chain(() => attempt(remaining - 1, delay * 2))\n            )\n      )\n    )\n\n  return attempt(maxAttempts, baseDelayMs)\n}\n\n\u002F\u002F Usage\nconst fetchUserWithRetry = (userId: string) =>\n  retry(fetchUser(userId), 3, 1000)\n  \u002F\u002F Attempts: immediate, 1s, 2s delays between retries\n```\n\n### Default Values\n\n```typescript\n\u002F\u002F Get value or use default (removes the error channel)\nconst getUsernameOrDefault = (userId: string) =>\n  pipe(\n    fetchUser(userId),\n    TE.map(user => user.name),\n    TE.getOrElse(() => T.of('Anonymous'))\n  ) \u002F\u002F Task\u003Cstring> - no more error tracking\n\n\u002F\u002F Keep error channel but provide fallback value\nconst getUserWithDefault = (userId: string) =>\n  pipe(\n    fetchUser(userId),\n    TE.orElse(() => TE.right(defaultUser))\n  ) \u002F\u002F TaskEither\u003CError, User> - error channel still exists but always succeeds\n```\n\n---\n\n## 5. Real API Examples\n\n### Complete Fetch Wrapper\n\n```typescript\n\u002F\u002F types.ts\ninterface ApiError {\n  code: string\n  message: string\n  status: number\n  details?: unknown\n}\n\n\u002F\u002F api.ts\nconst createApiError = (\n  code: string,\n  message: string,\n  status: number,\n  details?: unknown\n): ApiError => ({ code, message, status, details })\n\nconst request = \u003CT>(\n  url: string,\n  options: RequestInit = {}\n): TE.TaskEither\u003CApiError, T> =>\n  TE.tryCatch(\n    async () => {\n      const response = await fetch(url, {\n        headers: {\n          'Content-Type': 'application\u002Fjson',\n          ...options.headers,\n        },\n        ...options,\n      })\n\n      if (!response.ok) {\n        const body = await response.json().catch(() => ({}))\n        throw createApiError(\n          body.code || 'HTTP_ERROR',\n          body.message || response.statusText,\n          response.status,\n          body\n        )\n      }\n\n      \u002F\u002F Handle 204 No Content\n      if (response.status === 204) {\n        return undefined as T\n      }\n\n      return response.json()\n    },\n    (error): ApiError => {\n      if (typeof error === 'object' && error !== null && 'code' in error) {\n        return error as ApiError\n      }\n      return createApiError(\n        'NETWORK_ERROR',\n        error instanceof Error ? error.message : 'Request failed',\n        0\n      )\n    }\n  )\n\n\u002F\u002F API client\nconst api = {\n  get: \u003CT>(url: string) => request\u003CT>(url),\n\n  post: \u003CT>(url: string, body: unknown) =>\n    request\u003CT>(url, {\n      method: 'POST',\n      body: JSON.stringify(body)\n    }),\n\n  put: \u003CT>(url: string, body: unknown) =>\n    request\u003CT>(url, {\n      method: 'PUT',\n      body: JSON.stringify(body)\n    }),\n\n  delete: (url: string) =>\n    request\u003Cvoid>(url, { method: 'DELETE' }),\n}\n\n\u002F\u002F Usage\nconst getUser = (id: string) => api.get\u003CUser>(`\u002Fapi\u002Fusers\u002F${id}`)\nconst createUser = (data: CreateUserDto) => api.post\u003CUser>('\u002Fapi\u002Fusers', data)\nconst updateUser = (id: string, data: UpdateUserDto) => api.put\u003CUser>(`\u002Fapi\u002Fusers\u002F${id}`, data)\nconst deleteUser = (id: string) => api.delete(`\u002Fapi\u002Fusers\u002F${id}`)\n```\n\n### Database Operations (Prisma Example)\n\n```typescript\nimport { PrismaClient, Prisma } from '@prisma\u002Fclient'\n\ntype DbError =\n  | { _tag: 'NotFound'; entity: string; id: string }\n  | { _tag: 'UniqueViolation'; field: string }\n  | { _tag: 'ConnectionError'; cause: unknown }\n\nconst prisma = new PrismaClient()\n\nconst wrapPrisma = \u003CT>(\n  operation: () => Promise\u003CT>\n): TE.TaskEither\u003CDbError, T> =>\n  TE.tryCatch(\n    operation,\n    (error): DbError => {\n      if (error instanceof Prisma.PrismaClientKnownRequestError) {\n        if (error.code === 'P2002') {\n          const field = (error.meta?.target as string[])?.join(', ') || 'unknown'\n          return { _tag: 'UniqueViolation', field }\n        }\n        if (error.code === 'P2025') {\n          return { _tag: 'NotFound', entity: 'Record', id: 'unknown' }\n        }\n      }\n      return { _tag: 'ConnectionError', cause: error }\n    }\n  )\n\n\u002F\u002F Repository pattern\nconst userRepository = {\n  findById: (id: string): TE.TaskEither\u003CDbError, User> =>\n    pipe(\n      wrapPrisma(() => prisma.user.findUnique({ where: { id } })),\n      TE.chain(user =>\n        user\n          ? TE.right(user)\n          : TE.left({ _tag: 'NotFound', entity: 'User', id })\n      )\n    ),\n\n  findByEmail: (email: string): TE.TaskEither\u003CDbError, User | null> =>\n    wrapPrisma(() => prisma.user.findUnique({ where: { email } })),\n\n  create: (data: CreateUserInput): TE.TaskEither\u003CDbError, User> =>\n    wrapPrisma(() => prisma.user.create({ data })),\n\n  update: (id: string, data: UpdateUserInput): TE.TaskEither\u003CDbError, User> =>\n    wrapPrisma(() => prisma.user.update({ where: { id }, data })),\n\n  delete: (id: string): TE.TaskEither\u003CDbError, void> =>\n    pipe(\n      wrapPrisma(() => prisma.user.delete({ where: { id } })),\n      TE.map(() => undefined)\n    ),\n}\n\n\u002F\u002F Service using repository\nconst createUserService = (input: CreateUserInput) =>\n  pipe(\n    \u002F\u002F Check email doesn't exist\n    userRepository.findByEmail(input.email),\n    TE.chain(existing =>\n      existing\n        ? TE.left({ _tag: 'UniqueViolation' as const, field: 'email' })\n        : TE.right(undefined)\n    ),\n    \u002F\u002F Create user\n    TE.chain(() => userRepository.create(input))\n  )\n```\n\n### File Operations (Node.js)\n\n```typescript\nimport * as fs from 'fs\u002Fpromises'\nimport * as path from 'path'\n\ntype FileError =\n  | { _tag: 'NotFound'; path: string }\n  | { _tag: 'PermissionDenied'; path: string }\n  | { _tag: 'IoError'; cause: unknown }\n\nconst toFileError = (error: unknown, filePath: string): FileError => {\n  if (error instanceof Error) {\n    if ('code' in error) {\n      if (error.code === 'ENOENT') return { _tag: 'NotFound', path: filePath }\n      if (error.code === 'EACCES') return { _tag: 'PermissionDenied', path: filePath }\n    }\n  }\n  return { _tag: 'IoError', cause: error }\n}\n\nconst readFile = (filePath: string): TE.TaskEither\u003CFileError, string> =>\n  TE.tryCatch(\n    () => fs.readFile(filePath, 'utf-8'),\n    (e) => toFileError(e, filePath)\n  )\n\nconst writeFile = (filePath: string, content: string): TE.TaskEither\u003CFileError, void> =>\n  TE.tryCatch(\n    () => fs.writeFile(filePath, content, 'utf-8'),\n    (e) => toFileError(e, filePath)\n  )\n\nconst readJson = \u003CT>(filePath: string): TE.TaskEither\u003CFileError | { _tag: 'ParseError'; cause: unknown }, T> =>\n  pipe(\n    readFile(filePath),\n    TE.chain(content =>\n      TE.tryCatch(\n        () => Promise.resolve(JSON.parse(content)),\n        (e): { _tag: 'ParseError'; cause: unknown } => ({ _tag: 'ParseError', cause: e })\n      )\n    )\n  )\n\n\u002F\u002F Usage: Load config with fallback\nconst loadConfig = () =>\n  pipe(\n    readJson\u003CConfig>('.\u002Fconfig.json'),\n    TE.orElse(() => readJson\u003CConfig>('.\u002Fconfig.default.json')),\n    TE.getOrElse(() => T.of(defaultConfig))\n  )\n```\n\n---\n\n## 6. Handling Results\n\n### Pattern Matching with fold\u002Fmatch\n\n```typescript\n\u002F\u002F fold: Handle both success and failure, returns a Task (no more error channel)\nconst displayResult = pipe(\n  fetchUser(userId),\n  TE.fold(\n    (error) => T.of(`Error: ${error.message}`),\n    (user) => T.of(`Welcome, ${user.name}!`)\n  )\n) \u002F\u002F Task\u003Cstring>\n\n\u002F\u002F Execute and get the string\nconst message = await displayResult()\n```\n\n### Getting the Raw Either\n\n```typescript\n\u002F\u002F Sometimes you need to work with the Either directly\nconst result = await fetchUser(userId)() \u002F\u002F Either\u003CError, User>\n\nif (E.isLeft(result)) {\n  console.error('Failed:', result.left)\n} else {\n  console.log('User:', result.right)\n}\n```\n\n### In Express\u002FHono Handlers\n\n```typescript\n\u002F\u002F Express\napp.get('\u002Fusers\u002F:id', async (req, res) => {\n  const result = await pipe(\n    fetchUser(req.params.id),\n    TE.fold(\n      (error) => T.of({ status: 500, body: { error: error.message } }),\n      (user) => T.of({ status: 200, body: user })\n    )\n  )()\n\n  res.status(result.status).json(result.body)\n})\n\n\u002F\u002F Cleaner with a helper\nconst sendResult = \u003CE, A>(\n  res: Response,\n  te: TE.TaskEither\u003CE, A>,\n  errorStatus: number = 500\n) =>\n  pipe(\n    te,\n    TE.fold(\n      (error) => T.of(res.status(errorStatus).json({ error })),\n      (data) => T.of(res.json(data))\n    )\n  )()\n\napp.get('\u002Fusers\u002F:id', async (req, res) => {\n  await sendResult(res, fetchUser(req.params.id), 404)\n})\n```\n\n---\n\n## 7. Common Patterns Reference\n\n### Quick Transformations\n\n```typescript\n\u002F\u002F Transform success value\nTE.map(user => user.name)\n\n\u002F\u002F Transform error\nTE.mapLeft(error => ({ ...error, timestamp: Date.now() }))\n\n\u002F\u002F Transform both at once\nTE.bimap(\n  error => enhanceError(error),\n  user => user.profile\n)\n```\n\n### Filtering\n\n```typescript\n\u002F\u002F Fail if condition not met\npipe(\n  fetchUser(userId),\n  TE.filterOrElse(\n    user => user.isActive,\n    user => new Error(`User ${user.id} is not active`)\n  )\n)\n```\n\n### Side Effects Without Changing Value\n\n```typescript\n\u002F\u002F Log on success, keep the value unchanged\npipe(\n  fetchUser(userId),\n  TE.tap(user => TE.fromIO(() => console.log(`Fetched user: ${user.id}`)))\n)\n\n\u002F\u002F Log on error, keep the error unchanged\npipe(\n  fetchUser(userId),\n  TE.tapError(error => TE.fromIO(() => console.error(`Failed: ${error.message}`)))\n)\n\n\u002F\u002F chainFirst is like tap but for operations that return TaskEither\npipe(\n  createUser(userData),\n  TE.chainFirst(user => sendWelcomeEmail(user.email))\n) \u002F\u002F Returns the created user, not the email result\n```\n\n### Converting From Other Types\n\n```typescript\n\u002F\u002F From Either\nconst fromEither = TE.fromEither(E.right(42))\n\n\u002F\u002F From Option\nimport * as O from 'fp-ts\u002FOption'\nconst fromOption = TE.fromOption(() => new Error('Value was None'))\nconst result = fromOption(O.some(42))\n\n\u002F\u002F From boolean\nconst fromBoolean = TE.fromPredicate(\n  (x: number) => x > 0,\n  () => new Error('Must be positive')\n)\n```\n\n---\n\n## Quick Reference Card\n\n| What you want | How to do it |\n|---------------|--------------|\n| Wrap a promise | `TE.tryCatch(() => promise, toError)` |\n| Create success | `TE.right(value)` |\n| Create failure | `TE.left(error)` |\n| Transform value | `TE.map(fn)` |\n| Transform error | `TE.mapLeft(fn)` |\n| Chain async ops | `TE.chain(fn)` or `TE.flatMap(fn)` |\n| Run in parallel | `sequenceT(TE.ApplyPar)(te1, te2, te3)` |\n| Array in parallel | `TE.traverseArray(fn)(items)` |\n| Recover from error | `TE.orElse(fn)` |\n| Use default value | `TE.getOrElse(() => T.of(default))` |\n| Handle both cases | `TE.fold(onError, onSuccess)` |\n| Build up context | `TE.Do` + `TE.bind('name', () => te)` |\n| Log without changing | `TE.tap(fn)` |\n| Filter with error | `TE.filterOrElse(pred, toError)` |\n\n---\n\n## Before\u002FAfter Summary\n\n### Fetching Data\n\n```typescript\n\u002F\u002F BEFORE\nasync function getUser(id: string) {\n  try {\n    const res = await fetch(`\u002Fapi\u002Fusers\u002F${id}`)\n    if (!res.ok) throw new Error('Not found')\n    return await res.json()\n  } catch (e) {\n    console.error(e)\n    return null\n  }\n}\n\n\u002F\u002F AFTER\nconst getUser = (id: string) =>\n  TE.tryCatch(\n    async () => {\n      const res = await fetch(`\u002Fapi\u002Fusers\u002F${id}`)\n      if (!res.ok) throw new Error('Not found')\n      return res.json()\n    },\n    E.toError\n  )\n```\n\n### Chained Operations\n\n```typescript\n\u002F\u002F BEFORE\nasync function processOrder(orderId: string) {\n  const order = await fetchOrder(orderId)\n  if (!order) throw new Error('No order')\n  const user = await fetchUser(order.userId)\n  if (!user) throw new Error('No user')\n  const result = await chargePayment(user, order.total)\n  return result\n}\n\n\u002F\u002F AFTER\nconst processOrder = (orderId: string) =>\n  pipe(\n    TE.Do,\n    TE.bind('order', () => fetchOrder(orderId)),\n    TE.bind('user', ({ order }) => fetchUser(order.userId)),\n    TE.chain(({ user, order }) => chargePayment(user, order.total))\n  )\n```\n\n### Error Recovery\n\n```typescript\n\u002F\u002F BEFORE\nasync function getData(id: string) {\n  try {\n    return await fetchFromApi(id)\n  } catch {\n    try {\n      return await fetchFromCache(id)\n    } catch {\n      return defaultValue\n    }\n  }\n}\n\n\u002F\u002F AFTER\nconst getData = (id: string) =>\n  pipe(\n    fetchFromApi(id),\n    TE.orElse(() => fetchFromCache(id)),\n    TE.getOrElse(() => T.of(defaultValue))\n  )\n```\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,81,987,"2026-05-16 13:18:29",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"编程开发","coding","mdi-code-braces","代码生成、调试、审查，提升开发效率",2,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":25,"skillCount":32,"createdAt":26},"后端开发","backend","mdi-server","API、数据库、服务端架构",296,[34],{"id":35,"skillId":4,"version":36,"fileName":37,"fileSize":38,"filePath":39,"fileHash":40,"manifest":41,"createdAt":19},"f8e19a52-530f-47df-9be1-e7f8c0e43ccb","1.0.0","fp-async.zip",7434,"uploads\u002Fskills\u002F1e1c4a7d-a4af-410c-96ca-834e62778ac9\u002Ffp-async.zip","62be6c38413197cc98d73f09a1c28fe1288a2c15da7cd13bb689aaca86b1e247","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":24399}]",{"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]