sickn33/antigravity-awesome-skills/fp-async

Trust tier 1
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Practical async patterns using TaskEither - clean pipelines instead of try/catch hell, with real API examples

Install

npx vskill@latest install sickn33/antigravity-awesome-skills/fp-async

SKILL.md

Practical Async Patterns with fp-ts

Stop writing nested try/catch blocks. Stop losing error context. Start building clean async pipelines that handle errors properly.

TaskEither is simply an async operation that tracks success or failure. That's it. No fancy terminology needed.

Detailed Guide

Read the detailed guide before executing this skill. It retains the complete procedure and reference material. Treat its safety, prerequisites, and validation requirements as mandatory. For focused work, load the relevant sections; for end-to-end work, read the guide completely.

When to Use

  • You need async error handling in TypeScript with TaskEither.
  • The task involves wrapping Promises, composing API calls, or replacing nested try/catch flows.
  • You want practical fp-ts async patterns instead of academic explanations.
typescript
// TaskEither<Error, User> means:
// "An async operation that either fails with Error or succeeds with User"

5. Real API Examples

Complete Fetch Wrapper
typescript
// types.ts
interface ApiError {
code: string
message: string
status: number
details?: unknown
}
// api.ts
const createApiError = (
code: string,
message: string,
status: number,
details?: unknown
): ApiError => ({ code, message, status, details })
const request = <T>(
url: string,
options: RequestInit = {}
): TE.TaskEither<ApiError, T> =>
TE.tryCatch(
async () => {
const headers = new Headers(options.headers)
if (options.body != null && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json')
const response = await fetch(url, {
...options, headers,
signal: options.signal ?? AbortSignal.timeout(10000),
})
if (!response.ok) {
const parsed: unknown = await response.json().catch(() => ({}))
const body = typeof parsed === 'object' && parsed !== null
? parsed as Record<string, unknown> : {}
throw createApiError(
typeof body.code === 'string' ? body.code : 'HTTP_ERROR',
typeof body.message === 'string' ? body.message : response.statusText,
response.status,
body
)
}
// Handle 204 No Content
if (response.status === 204) {
throw createApiError("EMPTY_RESPONSE", "Expected JSON; use a separate void decoder for 204", 204)
}
return response.json() // Decode unknown with a runtime schema before trusting T
},
(error): ApiError => {
if (typeof error === 'object' && error !== null && 'code' in error) {
return error as ApiError
}
return createApiError(
'NETWORK_ERROR',
error instanceof Error ? error.message : 'Request failed',
0
)
}
)
// API client
const api = {
get: <T>(url: string) => request<T>(url),
post: <T>(url: string, body: unknown) =>
request<T>(url, {
method: 'POST',
body: JSON.stringify(body)
}),
put: <T>(url: string, body: unknown) =>
request<T>(url, {
method: 'PUT',
body: JSON.stringify(body)
}),
delete: (url: string) =>
request<void>(url, { method: 'DELETE' }),
}
// Usage
const getUser = (id: string) => api.get<User>(`/api/users/${encodeURIComponent(id)}`)
const createUser = (data: CreateUserDto) => api.post<User>('/api/users', data)
const updateUser = (id: string, data: UpdateUserDto) => api.put<User>(`/api/users/${encodeURIComponent(id)}`, data)
const deleteUser = (id: string) => api.delete(`/api/users/${encodeURIComponent(id)}`)
Database Operations (Prisma Example)
typescript
import { PrismaClient, Prisma } from '@prisma/client'
type DbError =
| { _tag: 'NotFound'; entity: string; id: string }
| { _tag: 'UniqueViolation'; field: string }
| { _tag: 'ConnectionError'; cause: unknown }
const prisma = new PrismaClient()
const wrapPrisma = <T>(
operation: () => Promise<T>
): TE.TaskEither<DbError, T> =>
TE.tryCatch(
operation,
(error): DbError => {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
const target = error.meta?.target
const field = Array.isArray(target) && target.every(x => typeof x === 'string')
? target.join(', ') : 'unknown'
return { _tag: 'UniqueViolation', field }
}
if (error.code === 'P2025') {
return { _tag: 'NotFound', entity: 'Record', id: 'unknown' }
}
}
return { _tag: 'ConnectionError', cause: error }
}
)
// Repository pattern
const userRepository = {
findById: (id: string): TE.TaskEither<DbError, User> =>
pipe(
wrapPrisma(() => prisma.user.findUnique({ where: { id } })),
TE.chain(user =>
user
? TE.right(user)
: TE.left({ _tag: 'NotFound', entity: 'User', id })
)
),
findByEmail: (email: string): TE.TaskEither<DbError, User | null> =>
wrapPrisma(() => prisma.user.findUnique({ where: { email } })),
create: (data: CreateUserInput): TE.TaskEither<DbError, User> =>
wrapPrisma(() => prisma.user.create({ data })),
update: (id: string, data: UpdateUserInput): TE.TaskEither<DbError, User> =>
wrapPrisma(() => prisma.user.update({ where: { id }, data })),
delete: (id: string): TE.TaskEither<DbError, void> =>
pipe(
wrapPrisma(() => prisma.user.delete({ where: { id } })),
TE.map(() => undefined)
),
}
// Service using repository
const createUserService = (input: CreateUserInput) =>
pipe(
// Check email doesn't exist
userRepository.findByEmail(input.email),
TE.chain(existing =>
existing
? TE.left({ _tag: 'UniqueViolation' as const, field: 'email' })
: TE.right(undefined)
),
// Create user
TE.chain(() => userRepository.create(input))
)
File Operations (Node.js)
typescript
import * as fs from 'fs/promises'
import * as path from 'path'
type FileError =
| { _tag: 'NotFound'; path: string }
| { _tag: 'PermissionDenied'; path: string }
| { _tag: 'IoError'; cause: unknown }
const toFileError = (error: unknown, filePath: string): FileError => {
if (error instanceof Error) {
if ('code' in error) {
if (error.code === 'ENOENT') return { _tag: 'NotFound', path: filePath }
if (error.code === 'EACCES') return { _tag: 'PermissionDenied', path: filePath }
}
}
return { _tag: 'IoError', cause: error }
}
const readFile = (filePath: string): TE.TaskEither<FileError, string> =>
TE.tryCatch(
() => fs.readFile(filePath, 'utf-8'),
(e) => toFileError(e, filePath)
)
const writeFile = (filePath: string, content: string): TE.TaskEither<FileError, void> =>
TE.tryCatch(
() => fs.writeFile(filePath, content, 'utf-8'),
(e) => toFileError(e, filePath)
)
const readJson = <T>(filePath: string): TE.TaskEither<FileError | { _tag: 'ParseError'; cause: unknown }, T> =>
pipe(
readFile(filePath),
TE.chainW(content =>
TE.tryCatch(
() => Promise.resolve(JSON.parse(content)),
(e): { _tag: 'ParseError'; cause: unknown } => ({ _tag: 'ParseError', cause: e })
)
)
)
// Usage: Load config with fallback
const loadConfig = () =>
pipe(
readJson<Config>('./config.json'),
TE.orElse(() => readJson<Config>('./config.default.json')),
TE.getOrElse(() => T.of(defaultConfig))
)

Limitations

  • Use this skill only when the task clearly matches the scope described above.
  • Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
  • Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.

Execution and data boundaries

These are separate fp-ts 2.x teaching fragments, with omitted imports, domain types, runtime decoders and application helpers. TaskEither is lazy: calling the returned task again repeats effects, and a pre-created Promise is not a fresh retry. The corrected batch traversal is sequential between batches; parallel traversal does not cancel in-flight work. Validate unknown JSON instead of asserting a generic T, and handle no-content responses through an explicit void decoder.

A pipeline is not a database transaction or payment saga: preserve inventory checks, use durable/provider idempotency, and reconcile failed shipment or ambiguous charge outcomes. Keep uniqueness constraints even after a pre-check. Retry only classified transient, safely repeatable operations with an overall deadline and jitter; the generic example retries every Left. Do not recover authorization, malformed security configuration or invalid data with defaults; branch on typed status rather than message substrings. File paths need an authorized root, and writes currently overwrite existing files. Map errors to safe public status/messages without serializing raw causes or database details.

Security

  • Security scanPassed, 46 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
23,385
Repository forks
4,018
Repository npm downloads, 7 days
11,772
7-day trend score
72

Stars and forks are for the whole GitHub repository, not this skill alone. Downloads are for the repository's npm package. Refreshed 19 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/sickn33/antigravity-awesome-skills/fp-async/badge)](https://verified-skill.com/skills/sickn33/antigravity-awesome-skills/fp-async)

Something wrong with this skill or its page? Tell us.