{"id":"71293921-c3e8-42fb-ad75-69e318dfa714","shortId":"vYsCDc","kind":"skill","title":"fp-async","tagline":"Practical async patterns using TaskEither - clean pipelines instead of try/catch hell, with real API examples","description":"# Practical Async Patterns with fp-ts\n\nStop writing nested try/catch 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/catch` flows.\n- You want practical fp-ts async patterns instead of academic explanations.\n\n```typescript\n// TaskEither<Error, User> means:\n// \"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/Catch Everywhere\n\n```typescript\n// BEFORE: Try/catch hell\nasync function getUserData(userId: string) {\n  try {\n    const response = await fetch(`/api/users/${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(`/api/users/${userId}/posts`)\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      // Now what? Return partial data? Rethrow? Log?\n      console.error('Failed to fetch posts:', postsError)\n      return { user, posts: [] }\n    }\n  } catch (error) {\n    // 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/TaskEither'\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\n\n// One wrapper function - reuse everywhere\nconst fetchJson = <T>(url: string): TE.TaskEither<Error, 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// AFTER: Clean and composable\nconst getUser = (userId: string) => fetchJson<User>(`/api/users/${userId}`)\nconst getPosts = (userId: string) => fetchJson<Post[]>(`/api/users/${userId}/posts`)\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,           // The async work\n  (thrown) => toError(thrown)  // Convert failures to your error type\n)\n```\n\n### Creating Success and Failure Values\n\n```typescript\n// Wrap a value as success\nconst success = TE.right<Error, number>(42)\n\n// Wrap a value as failure\nconst failure = TE.left<Error, number>(new Error('Nope'))\n\n// From a nullable value (null/undefined becomes error)\nconst fromNullable = TE.fromNullable(new Error('Value was null'))\nconst result = fromNullable(maybeUser) // TaskEither<Error, User>\n\n// 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 / Nested Awaits\n\n```typescript\n// 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            // 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// 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())  // 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))  // Returns another TaskEither\n)\n```\n\n### Building Context with Do Notation\n\nWhen you need values from multiple steps:\n\n```typescript\n// 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// 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// Operations depend on each other - must be sequential\nconst getUserWithOrg = (userId: string) =>\n  pipe(\n    fetchUser(userId),                              // First: get user\n    TE.chain(user => fetchTeam(user.teamId)),      // Then: get their team\n    TE.chain(team => fetchOrganization(team.orgId)) // Finally: get org\n  )\n```\n\n### Parallel Execution\n\n```typescript\nimport { sequenceT } from 'fp-ts/Apply'\n\n// Independent operations - run in parallel\nconst getDashboardData = (userId: string) =>\n  sequenceT(TE.ApplyPar)(\n    fetchUser(userId),\n    fetchNotifications(userId),\n    fetchRecentActivity(userId)\n  ) // Returns TaskEither<Error, [User, Notification[], Activity[]]>\n\n// 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// Fetch multiple users in parallel\nconst userIds = ['1', '2', '3', '4', '5']\n\n// TE.traverseArray runs all fetches in parallel\nconst fetchAllUsers = pipe(\n  userIds,\n  TE.traverseArray(fetchUser)\n) // TaskEither<Error, readonly User[]>\n\n// Note: Fails fast - if ANY request fails, the whole thing fails\n// 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 = <T>(arr: T[], size: number): T[][] => {\n  const chunks: T[][] = []\n  for (let i = 0; i < arr.length; i += size) {\n    chunks.push(arr.slice(i, i + size))\n  }\n  return chunks\n}\n\n// Process in batches of 5 concurrent requests\nconst fetchUsersWithLimit = (userIds: string[]) => {\n  const batches = chunk(userIds, 5)\n\n  return pipe(\n    batches,\n    // Process batches sequentially\n    TE.traverseArray(batch =>\n      // 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// WRONG: This looks parallel but order might matter for DB operations\nconst createUserAndProfile = (userData: UserData) =>\n  sequenceT(TE.ApplyPar)(\n    createUser(userData),           // Creates user with ID\n    createProfile(userData.profile) // Needs user ID - race condition!\n  )\n\n// 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// Try primary API, fall back to cache\nconst getUserWithFallback = (userId: string) =>\n  pipe(\n    fetchUserFromApi(userId),\n    TE.orElse(() => fetchUserFromCache(userId))\n  )\n\n// 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// 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)  // 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<ApiError, User> =>\n  TE.tryCatch(\n    async () => {\n      const res = await fetch(`/api/users/${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// 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) // Propagate auth errors\n        case 'NetworkError':\n          return fetchUserFromCache(userId) // Try cache on network issues\n      }\n    })\n  )\n```\n\n### Retry with Exponential Backoff\n\n```typescript\nimport * as T from 'fp-ts/Task'\n\nconst wait = (ms: number): T.Task<void> =>\n  () => new Promise(resolve => setTimeout(resolve, ms))\n\nconst retry = <E, A>(\n  operation: TE.TaskEither<E, A>,\n  maxAttempts: number,\n  baseDelayMs: number = 1000\n): TE.TaskEither<E, A> => {\n  const attempt = (remaining: number, delay: number): TE.TaskEither<E, A> =>\n    pipe(\n      operation,\n      TE.orElse(error =>\n        remaining <= 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// Usage\nconst fetchUserWithRetry = (userId: string) =>\n  retry(fetchUser(userId), 3, 1000)\n  // Attempts: immediate, 1s, 2s delays between retries\n```\n\n### Default Values\n\n```typescript\n// 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  ) // Task<string> - no more error tracking\n\n// Keep error channel but provide fallback value\nconst getUserWithDefault = (userId: string) =>\n  pipe(\n    fetchUser(userId),\n    TE.orElse(() => TE.right(defaultUser))\n  ) // TaskEither<Error, 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// types.ts\ninterface ApiError {\n  code: string\n  message: string\n  status: number\n  details?: unknown\n}\n\n// 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 = <T>(\n  url: string,\n  options: RequestInit = {}\n): TE.TaskEither<ApiError, T> =>\n  TE.tryCatch(\n    async () => {\n      const response = await fetch(url, {\n        headers: {\n          'Content-Type': 'application/json',\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      // 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// API client\nconst api = {\n  get: <T>(url: string) => request<T>(url),\n\n  post: <T>(url: string, body: unknown) =>\n    request<T>(url, {\n      method: 'POST',\n      body: JSON.stringify(body)\n    }),\n\n  put: <T>(url: string, body: unknown) =>\n    request<T>(url, {\n      method: 'PUT',\n      body: JSON.stringify(body)\n    }),\n\n  delete: (url: string) =>\n    request<void>(url, { method: 'DELETE' }),\n}\n\n// Usage\nconst getUser = (id: string) => api.get<User>(`/api/users/${id}`)\nconst createUser = (data: CreateUserDto) => api.post<User>('/api/users', data)\nconst updateUser = (id: string, data: UpdateUserDto) => api.put<User>(`/api/users/${id}`, data)\nconst deleteUser = (id: string) => api.delete(`/api/users/${id}`)\n```\n\n### Database Operations (Prisma Example)\n\n```typescript\nimport { PrismaClient, Prisma } from '@prisma/client'\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 = <T>(\n  operation: () => Promise<T>\n): TE.TaskEither<DbError, 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// Repository pattern\nconst userRepository = {\n  findById: (id: string): TE.TaskEither<DbError, 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<DbError, User | null> =>\n    wrapPrisma(() => prisma.user.findUnique({ where: { email } })),\n\n  create: (data: CreateUserInput): TE.TaskEither<DbError, User> =>\n    wrapPrisma(() => prisma.user.create({ data })),\n\n  update: (id: string, data: UpdateUserInput): TE.TaskEither<DbError, User> =>\n    wrapPrisma(() => prisma.user.update({ where: { id }, data })),\n\n  delete: (id: string): TE.TaskEither<DbError, void> =>\n    pipe(\n      wrapPrisma(() => prisma.user.delete({ where: { id } })),\n      TE.map(() => undefined)\n    ),\n}\n\n// Service using repository\nconst createUserService = (input: CreateUserInput) =>\n  pipe(\n    // 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    // Create user\n    TE.chain(() => userRepository.create(input))\n  )\n```\n\n### File Operations (Node.js)\n\n```typescript\nimport * as fs from 'fs/promises'\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<FileError, 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<FileError, void> =>\n  TE.tryCatch(\n    () => fs.writeFile(filePath, content, 'utf-8'),\n    (e) => toFileError(e, filePath)\n  )\n\nconst readJson = <T>(filePath: string): TE.TaskEither<FileError | { _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// Usage: Load config with fallback\nconst loadConfig = () =>\n  pipe(\n    readJson<Config>('./config.json'),\n    TE.orElse(() => readJson<Config>('./config.default.json')),\n    TE.getOrElse(() => T.of(defaultConfig))\n  )\n```\n\n---\n\n## 6. Handling Results\n\n### Pattern Matching with fold/match\n\n```typescript\n// 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) // Task<string>\n\n// Execute and get the string\nconst message = await displayResult()\n```\n\n### Getting the Raw Either\n\n```typescript\n// Sometimes you need to work with the Either directly\nconst result = await fetchUser(userId)() // Either<Error, 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/Hono Handlers\n\n```typescript\n// Express\napp.get('/users/: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// Cleaner with a helper\nconst sendResult = <E, A>(\n  res: Response,\n  te: TE.TaskEither<E, 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('/users/: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// Transform success value\nTE.map(user => user.name)\n\n// Transform error\nTE.mapLeft(error => ({ ...error, timestamp: Date.now() }))\n\n// Transform both at once\nTE.bimap(\n  error => enhanceError(error),\n  user => user.profile\n)\n```\n\n### Filtering\n\n```typescript\n// 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// 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// 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// chainFirst is like tap but for operations that return TaskEither\npipe(\n  createUser(userData),\n  TE.chainFirst(user => sendWelcomeEmail(user.email))\n) // Returns the created user, not the email result\n```\n\n### Converting From Other Types\n\n```typescript\n// From Either\nconst fromEither = TE.fromEither(E.right(42))\n\n// From Option\nimport * as O from 'fp-ts/Option'\nconst fromOption = TE.fromOption(() => new Error('Value was None'))\nconst result = fromOption(O.some(42))\n\n// 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/After Summary\n\n### Fetching Data\n\n```typescript\n// BEFORE\nasync function getUser(id: string) {\n  try {\n    const res = await fetch(`/api/users/${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// AFTER\nconst getUser = (id: string) =>\n  TE.tryCatch(\n    async () => {\n      const res = await fetch(`/api/users/${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// 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// 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// 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// 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.","tags":["async","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-async","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-async","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 (24,086 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:57.660Z","embedding":null,"createdAt":"2026-04-18T21:37:22.953Z","updatedAt":"2026-04-24T00:50:57.660Z","lastSeenAt":"2026-04-24T00:50:57.660Z","tsv":"'-8':1802,1821 '/api/users':138,156,283,291,1129,1522,1529,1538,1546,2294,2323 '/apply':796 '/config.default.json':1867 '/config.json':1864 '/either':228 '/function':235 '/option':2161 '/posts':158,293 '/task':1230 '/taskeither':220 '/users':1954,2014 '0':395,922,1475,2183 '1':116,300,858,1272,1282 '1000':1254,1298 '1s':1301 '2':307,403,859,1284 '200':1976 '204':1438,1443 '2s':1302 '3':702,860,1297 '4':861,1032 '401':1140 '404':1087,1133,2024 '42':350,2151,2174 '5':862,938,949,1363 '500':1969,1999 '6':1871 '7':2025 'academ':97 'access':662 'activ':819,838,841,2075 'altern':1038 'alway':1361 'anonym':1330 'anoth':612,714 'api':17,80,1042,1365,1476,1479 'api.delete':1545 'api.get':1521 'api.post':1528 'api.put':1537 'api.ts':1382 'apierror':1105,1121,1157,1168,1373,1393,1405,1451,1464 'app.get':1953,2013 'application/json':1418 'arr':911 'arr.length':924 'arr.slice':928 'array':848,2237 'ask':2468 'async':3,5,20,38,48,67,93,105,128,249,302,323,405,420,599,1124,1408,1956,2016,2222,2284,2318,2339,2402 'attempt':1259,1280,1286,1299 'auth':1206 'await':136,149,154,168,252,412,428,442,456,470,484,502,1127,1411,1425,1914,1932,1961,2019,2292,2304,2321,2346,2358,2370,2409,2415 'back':1044 'backoff':1221 'basedelaym':1252,1288 'batch':899,936,946,952,954,957,961,966 'becom':369 'before/after':2278 'block':30 'bodi':1424,1436,1488,1494,1496,1500,1506,1508,1970,1977 'body.code':1430 'body.message':1433 'boolean':2176 'boundari':2476 'build':36,614,2260 'cach':1046,1214 'call':81 'callback':409 'card':2191 'case':1195,1200,1208,2256 'catch':174,192,492,507,511,515,519,1427,2306,2412,2418 'caus':1112,1149,1171,1572,1621,1753,1789,1834,1849,1853 'chain':404,533,566,592,752,1057,2221,2335 'chainfirst':2115 'chang':2079,2269 'channel':1317,1338,1357,1891 'chargepay':471,557,649,682,2371,2395 'check':1703 'checkinventori':457,553 'chunk':910,917,933,947 'chunks.push':927 'clarif':2470 'clean':9,37,211,275,530 'cleaner':1983 'clear':2443 'client':1477 'code':1374,1385,1394,1458,1767 'common':2026 'complet':1367 'compos':79,277 'concurr':906,939 'condit':388,1007,1069,2059 'config':1857 'connectionerror':1571,1620 'console.error':183,200,521,1941,2112,2308 'console.log':1945,2095 'const':134,147,152,166,241,250,278,285,345,356,371,379,389,426,440,454,468,482,539,634,663,762,802,822,856,869,909,916,941,945,989,1015,1047,1060,1077,1116,1125,1184,1231,1242,1258,1290,1318,1343,1383,1398,1409,1423,1478,1517,1524,1531,1541,1574,1578,1596,1625,1698,1717,1755,1791,1807,1826,1860,1892,1912,1930,1959,1987,2147,2162,2170,2177,2290,2313,2319,2344,2356,2368,2377,2422 'content':1416,1440,1811,1819,1841,1845 'content-typ':1415 'context':34,196,615,2262 'control':900 'convert':311,328,2140 'creat':334,997,1660,1722,2134,2205,2209 'createapierror':1384,1429,1466 'createdefaultus':1091 'createguestus':1199 'createprofil':1001,1025 'createship':485,562,689 'createus':995,1020,1525,2126 'createuserandprofil':990,1016 'createuserdto':1527 'createuserinput':1662,1701 'createuserservic':1699 'criteria':2479 'data':180,1526,1530,1535,1540,1661,1668,1672,1681,2009,2012,2281 'databas':1548 'date.now':2044 'db':987 'dberror':1559,1583,1588,1631,1653,1664,1675,1686 'deepli':415 'default':1306,1313,2249,2253 'defaultconfig':1068,1870 'defaultus':1352 'defaultvalu':2420,2434 'delay':1262,1278,1283,1303 'delet':1509,1515,1682 'deleteus':1542 'depend':718,755,1014 'describ':2447 'destructur':821 'detail':1380,1391,1397 'differ':1183 'direct':1929 'displayresult':1893,1915 'doesn':1705 'e':223,493,506,508,510,512,514,516,518,520,525,527,1156,1159,1161,1165,1166,1172,1175,1179,1244,1248,1256,1265,1803,1805,1822,1824,1846,1854,1989,1995,2307,2309 'e.isleft':1939 'e.right':2150 'e.toerror':2334 'eacc':1780 'effect':2077 'either':108,1919,1928,1935,2146 'els':1944 'email':1650,1659,1704,1719,2138 'enhanceerror':2051 'enoent':1772 'entiti':1562,1614,1646 'environ':2459 'environment-specif':2458 'error':33,42,68,101,111,144,163,193,203,205,246,259,265,266,268,269,271,273,317,332,348,359,362,370,375,384,398,435,449,463,478,816,876,891,1033,1076,1085,1094,1099,1101,1113,1151,1174,1177,1182,1192,1204,1207,1270,1274,1316,1334,1337,1354,1356,1432,1450,1454,1456,1460,1462,1468,1469,1471,1587,1590,1622,1757,1763,1765,1769,1790,1890,1898,1900,1936,1966,1971,2003,2008,2039,2041,2042,2050,2052,2070,2101,2104,2110,2166,2185,2212,2218,2245,2274,2300,2329,2353,2365,2398 'error._tag':1194 'error.code':1594,1609,1771,1779 'error.message':1472,1901,1972,2114 'error.message.includes':1086,1088 'error.meta':1598 'errorstatus':1997,2006 'everyth':661 'everywher':123,240 'exampl':18,1366,1551 'execut':706,788,1907 'exist':1359,1707,1711,1712 'expect':399 'expert':2464 'explain':295 'explan':98 'exponenti':1220 'express':1952 'express/hono':1949 'fail':109,184,199,202,480,524,579,602,880,885,889,1474,1942,2057,2113 'failur':54,329,337,355,357,1884,2210 'fall':1043 'fallback':1036,1059,1341,1859 'fanci':59 'fast':881 'fetch':137,155,186,253,746,851,866,1128,1368,1412,2096,2280,2293,2322 'fetchallus':870 'fetchfromapi':2410,2427 'fetchfromcach':2416,2430 'fetchjson':242,282,289 'fetchnotif':810,831 'fetchord':429,544,609,639,671,2347,2385 'fetchorgan':782 'fetchrecentact':812,833 'fetchremoteconfig':1063 'fetchteam':774 'fetchus':443,548,582,605,644,676,767,808,829,874,968,1082,1117,1189,1295,1323,1348,1895,1933,1963,2022,2063,2090,2107,2359,2390 'fetchuserfromapi':1052 'fetchuserfromcach':1055,1211 'fetchuserorcr':1078 'fetchuserswithlimit':942 'fetchuserwithretri':1291 'field':1568,1597,1607,1718 'file':1727 'fileerror':1742,1761,1796,1814,1831 'filepath':1759,1777,1785,1793,1800,1806,1809,1818,1825,1828,1839 'filter':2055,2272 'final':784 'findbyemail':1649 'findbyid':1627 'first':769,894 'flat':536 'flatmap':594 'flow':86 'fn':2216,2220,2225,2228,2241,2247,2271 'fold':1879 'fold/match':1877 'follow':419 'found':438,452,1090,2302,2331 'fp':2,24,91,218,226,233,794,1228,2159 'fp-async':1 'fp-ts':23,90,217,225,232,793,1227,2158 'fromboolean':2178 'fromeith':2148 'fromnul':372,381 'fromopt':2163,2172 'fs':1733 'fs.readfile':1799 'fs.writefile':1817 'fs/promises':1735 'function':129,238,303,309,421,2285,2340,2403 'get':770,777,785,1309,1480,1909,1916 'getconfigrobust':1061 'getdashboard':823 'getdashboarddata':803 'getdata':2404,2423 'getpost':286 'getus':279,1518,2286,2314 'getuserdata':130 'getusernameordefault':1319 'getuserorguest':1185 'getuserwithdefault':1344 'getuserwithfallback':1048 'getuserwithorg':763 'got':401 'handl':41,69,210,1180,1437,1872,1880,2254 'handler':1950 'hard':417 'header':1414 'hell':14,127,410 'helper':1986 'http':145,164,260,1152,1431 'id':750,1000,1005,1108,1118,1130,1137,1519,1523,1533,1539,1543,1547,1564,1616,1628,1637,1648,1670,1680,1683,1692,1955,2015,2287,2295,2315,2324,2405,2411,2417,2424,2428,2431 'immedi':1300 'import':213,221,229,790,1223,1553,1731,1736,2154 'independ':740,797 'input':1700,1726,2473 'input.email':1709 'instanceof':267,1173,1470,1591,1764 'instead':11,95 'interfac':1372 'inventori':455,556 'inventory.available':460 'involv':76 'ioerror':1752,1788 'issu':1217 'item':2242 'join':1602 'json':1981,2007 'json.parse':1844 'json.stringify':1495,1507 'keep':660,1336,2085,2102 'length':846 'let':920 'like':2117 'limit':729,905,2435 'load':1856 'loadconfig':1861 'loadlocalconfig':1065 'log':182,496,2082,2099,2267 'look':975,980 'lose':32 'lost':194,896 'manual':633 'map':568,570 'match':1875,2444 'matter':732,985 'maxattempt':1250,1287 'maybeus':382 'mean':103 'messag':1376,1387,1395,1913 'met':2061 'method':1492,1504,1514 'might':305,984 'miss':2481 'ms':1233,1241 'multipl':624,747,852,1058 'must':759,2186 'mustbeposit':390 'n':392,394,396,402,844 'n.read':845 'name':2265 'need':61,66,621,725,903,1003,1923 'nest':28,84,411,416 'network':1216,1467 'networkerror':1111,1148,1170,1209 'new':143,162,258,270,361,374,397,434,448,462,477,1150,1176,1236,1576,2069,2165,2184,2299,2328,2352,2364 'node.js':1729 'none':2169 'nope':363 'notat':618,659 'note':879 'notfound':1107,1136,1196,1561,1613,1645,1744,1775 'notif':818,837,840 'notifications.filter':843 'null':378,1162,1457,1655,2311 'null/undefined':368 'nullabl':366 'number':349,360,393,914,1234,1251,1253,1261,1263,1379,1390,1998,2181 'o':2156 'o.some':2173 'object':1160,1455 'one':236,712 'onerror':2258 'onsuccess':2259 'op':2223 'oper':49,106,406,717,738,754,798,849,988,1246,1268,1549,1580,1586,1728,2121,2336 'option':1402,1420,2153 'options.headers':1419 'order':427,432,436,486,489,522,547,563,642,654,670,675,681,687,690,693,731,983,2345,2350,2355,2384,2389,2394 'order.id':697 'order.items':458,554 'order.total':473,559,651,684,2373,2397 'order.userid':444,549,645,677,2360,2391 'orderid':423,430,541,545,636,640,665,672,696,2342,2348,2379,2386 'org':786 'output':2453 'p2002':1595 'p2025':1610 'parallel':703,733,787,801,847,855,868,897,964,974,981,2231,2239 'parseerror':1833,1848,1852 'partial':179 'path':1738,1740,1745,1749,1776,1784 'pattern':6,21,94,1035,1624,1874,2027 'payment':469,479,491,495,561,565,653,656,679,694 'payment.id':504,699 'payment.success':475 'paymentid':698 'permiss':2474 'permissiondeni':1748,1783 'pipe':230,543,552,581,604,638,643,648,667,766,826,871,951,965,1019,1024,1051,1062,1081,1188,1267,1275,1322,1347,1633,1688,1702,1837,1862,1894,1962,2000,2062,2089,2106,2125,2381,2426 'pipelin':10,39,531,538 'posit':400,2188 'post':153,172,187,191,290,1485,1493 'posts.json':169 'posts.ok':160 'posts.status':165 'postsdata':167,173 'postserror':175,188 'practic':4,19,89 'pred':2276 'previous':721 'primari':1041 'prisma':1550,1555,1575 'prisma.prismaclientknownrequesterror':1592 'prisma.user.create':1667 'prisma.user.delete':1690 'prisma.user.findunique':1635,1657 'prisma.user.update':1678 'prisma/client':1557 'prismacli':1554,1577 'problem':121,408 'process':523,934,953 'processord':422,540,664,2341,2378 'processordermanu':635 'profil':1029,1031 'promis':78,118,1237,1581,2201,2203 'promise.resolve':1843 'propag':1205 'proper':43 'provid':1340 'put':1497,1505 'quick':2029,2189 'race':1006 'rate':728 'raw':1918 're':1096 're-throw':1095 'readabl':537 'readfil':1792,1838 'readjson':1827,1863,1866 'readon':877 'real':16,1364 'record':1615 'recov':1073,2243 'recoveri':1034,1070,1102,2399 'refer':2028,2190 'refund':494 'refundpay':503 'remain':1260,1271,1281 'remov':1314 'replac':83 'repositori':1623,1697 'req':1957,2017 'req.params.id':1964,2023 'request':884,907,940,1399,1473,1483,1490,1502,1512 'requestinit':1403 'requir':2472 'res':1126,1958,1991,2018,2021,2291,2320 'res.json':1155,2011,2305,2333 'res.ok':1145,2297,2326 'res.status':1132,1139,1153,1979,2005 'resolv':1238,1240 'resourc':748 'respect':727 'respons':135,251,1410,1992 'response.json':150,264,1426,1449 'response.ok':141,256,1422 'response.status':146,261,1435,1442 'response.statustext':262,1434 'result':380,722,970,1873,1931,1940,1960,2139,2171,2369,2375 'result.body':1982 'result.left':1943 'result.right':1947 'result.status':1980 'results.flat':971 'rethrow':181 'retri':1218,1243,1294,1305 'return':170,178,189,263,488,611,814,932,950,1154,1197,1202,1210,1285,1444,1448,1461,1465,1604,1611,1618,1773,1781,1786,1885,2123,2132,2303,2310,2332,2374,2408,2414,2419 'reus':239 'review':2465 'right':1008 'run':799,864,962,2229 'safe':119 'safeti':2475 'scope':2446 'sendresult':1988,2020 'sendwelcomeemail':2130 'sequencet':791,806,827,993,2232 'sequenti':705,711,751,761,955,972,1009 'servic':1695 'settimeout':1239 'shipment':483,490,686,695 'shipment.tracking':701 'side':2076 'simpli':46 'size':913,926,931 'skill':2438 'skill-fp-async' 'solut':207,529 'somepromis':321 'someth':201 'sometim':1921 'source-sickn33' 'specif':1075,1181,2460 'speed':744 'start':35 'state':500 'status':1378,1389,1396,1968,1975 'step':625 'still':1358 'stock':466 'stop':26,31,2466 'string':132,244,272,281,288,424,542,637,666,765,805,825,944,1050,1080,1109,1119,1178,1187,1293,1321,1346,1375,1377,1386,1388,1401,1482,1487,1499,1511,1520,1534,1544,1563,1565,1569,1601,1629,1651,1671,1684,1746,1750,1760,1794,1797,1810,1812,1829,1911,2288,2316,2343,2380,2406,2425 'substitut':2456 'succeed':113,1362 'success':52,335,344,346,1882,2033,2084,2206,2478 'summari':2279 'switch':1193 'synchron':575 't.of':1329,1869,1899,1903,1967,1974,2004,2010,2252,2433 't.task':1235 'tag':1106,1110,1114,1135,1142,1147,1163,1169,1560,1566,1570,1605,1612,1619,1644,1714,1743,1747,1751,1774,1782,1787,1832,1847,1851 'take':297 'tap':2118 'target':1599 'task':75,1331,1887,1906,2442 'taskeith':8,44,73,100,383,613,815,875,1353,2124 'te':215,1993,2001,2266 'te.applypar':807,828,994,2233 'te.bimap':2049 'te.bind':669,673,678,685,2264,2383,2387 'te.chain':546,550,555,560,607,641,646,772,780,1022,1279,1638,1710,1724,1840,2224,2392 'te.chainfirst':2128 'te.do':668,2263,2382 'te.filterorelse':2065,2275 'te.flatmap':2227 'te.fold':1897,1965,2002,2257 'te.fromeither':2149 'te.fromio':2094,2111 'te.fromnullable':373 'te.fromoption':2164 'te.frompredicate':391,2179 'te.fromtask':1276 'te.getorelse':1328,1868,2251,2432 'te.left':358,1093,1203,1273,1643,1713,2211 'te.map':584,652,692,835,969,1028,1325,1693,2035,2215 'te.mapleft':2040,2219 'te.orelse':1054,1064,1066,1084,1191,1269,1350,1865,2246,2429 'te.right':347,1067,1198,1351,1641,1720,2207 'te.tap':2092,2270 'te.taperror':2109 'te.taskeither':245,1120,1247,1255,1264,1404,1582,1630,1652,1663,1674,1685,1795,1813,1830,1994 'te.traversearray':863,873,956,967,2240 'te.trycatch':248,296,320,1123,1407,1585,1798,1816,1842,2202,2317 'te1':2234 'te2':2235 'te3':2236 'team':779,781 'team.orgid':783 'tempt':976 'terminolog':60 'test':2462 'thing':299,888 'thread':630 'throw':142,161,204,257,306,433,447,461,476,505,509,513,517,526,1097,1134,1141,1146,1428,2298,2327,2351,2363 'thrown':313,325,327 'timestamp':2043 'toerror':326,2204,2277 'tofileerror':1756,1804,1823 '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' 'track':51,1335 'trackingnumb':700 'transform':573,588,597,2030,2032,2038,2045,2213,2217 'treat':2451 'tri':133,151,425,439,453,467,481,1040,1213,2289,2407,2413 'try/catch':13,29,85,122,126 'trycatch':294 'ts':25,92,219,227,234,795,1229,2160 'two':298 'type':318,333,1100,1104,1417,1558,1741,2143 'typeof':1158,1453 'types.ts':1371 'typescript':71,99,124,212,319,339,413,534,580,603,626,753,789,850,908,977,1039,1071,1103,1222,1308,1370,1552,1730,1878,1920,1951,2031,2056,2081,2144,2282,2337,2400 'unauthor':1115,1143,1201 'unchang':2088,2105 'undefin':1445,1694,1721 'uniqueviol':1567,1606,1715 'unknown':1381,1392,1489,1501,1573,1603,1617,1754,1758,1835,1850 'unreadcount':842 'updat':1669 'updateus':1532 'updateuserdto':1536 'updateuserinput':1673 'url':243,254,1400,1413,1481,1484,1486,1491,1498,1503,1510,1513 'usag':1289,1516,1855 'use':7,64,569,591,709,1312,1696,2248,2436 'user':102,115,148,171,190,385,441,446,450,472,487,551,558,564,585,608,647,650,655,674,680,683,688,691,771,773,817,836,839,853,878,998,1004,1023,1030,1122,1326,1355,1632,1639,1640,1642,1647,1654,1665,1676,1723,1902,1937,1946,1973,1978,2036,2053,2066,2068,2071,2093,2097,2129,2135,2357,2362,2367,2372,2388,2393,2396 'user.email':2131 'user.id':610,1026,2072,2098 'user.isactive':2067 'user.name':1327,1905,2037 'user.name.touppercase':586 'user.profile':2054 'user.teamid':775 'userdata':991,992,996,1017,1018,1021,2127 'userdata.profile':1002,1027 'userid':131,139,157,280,284,287,292,583,606,764,768,804,809,811,813,824,830,832,834,857,872,943,948,1049,1053,1056,1079,1083,1092,1186,1190,1212,1292,1296,1320,1324,1345,1349,1896,1934,2064,2091,2108 'userrepositori':1626 'userrepository.create':1725 'userrepository.findbyemail':1708 'utf':1801,1820 'valid':2461 'valu':314,338,342,353,367,376,590,622,631,1307,1310,1342,2034,2080,2087,2167,2208,2214,2250 'void':1687,1815 'vs':567,704 'wait':1232,1277 'want':88,743,2194 'welcom':1904 'whole':887 'within':959 'without':2078,2268 'work':324,1925 'wrap':77,117,208,340,351,2199 'wrapper':237,1369 'wrapprisma':1579,1634,1656,1666,1677,1689 'write':27 'writefil':1808 'wrong':978 'x':2180,2182","prices":[{"id":"e193336f-2a4b-425b-a5b2-004f20dbc8ec","listingId":"71293921-c3e8-42fb-ad75-69e318dfa714","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:22.953Z"}],"sources":[{"listingId":"71293921-c3e8-42fb-ad75-69e318dfa714","source":"github","sourceId":"sickn33/antigravity-awesome-skills/fp-async","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-async","isPrimary":false,"firstSeenAt":"2026-04-18T21:37:22.953Z","lastSeenAt":"2026-04-24T00:50:57.660Z"}],"details":{"listingId":"71293921-c3e8-42fb-ad75-69e318dfa714","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"fp-async","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":"4960900cdb46a4fe041cbbc5518d34c9d79fc1ac","skill_md_path":"skills/fp-async/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-async"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"fp-async","description":"Practical async patterns using TaskEither - clean pipelines instead of try/catch hell, with real API examples"},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-async"},"updatedAt":"2026-04-24T00:50:57.660Z"}}