fp-async – skill Lập trình cho agent AI
Mẫu xử lý bất đồng bộ thực tế với TaskEither - pipeline gọn gàng thay vì try/catch rối rắm, kèm ví dụ API thực tế
Cài skill fp-async bằng một lệnh
Chọn công cụ bạn đang dùng, copy lệnh và dán vào terminal. Skill sẽ nằm trong thư mục skills của công cụ đó và sẵn sàng dùng ở phiên làm việc tiếp theo.
Claude Code
mkdir -p ~/.claude/skills && curl -fsSL https://skill.dn1nguoi.com/z/fp-async-6ebd6.tgz | tar xz -C ~/.claude/skills
Gọi trong Claude Code: /fp-async – rồi mô tả công việc bạn muốn làm.
Codex
mkdir -p ~/.codex/skills && curl -fsSL https://skill.dn1nguoi.com/z/fp-async-6ebd6.tgz | tar xz -C ~/.codex/skills
Gọi trong Codex: $fp-async – rồi mô tả công việc bạn muốn làm.
Hermes Agent
mkdir -p ~/.hermes/skills && curl -fsSL https://skill.dn1nguoi.com/z/fp-async-6ebd6.tgz | tar xz -C ~/.hermes/skills
Gọi trong Hermes Agent: /fp-async – rồi mô tả công việc bạn muốn làm.
DeepSeek
mkdir -p ~/.deepcode/skills && curl -fsSL https://skill.dn1nguoi.com/z/fp-async-6ebd6.tgz | tar xz -C ~/.deepcode/skills
Gọi trong DeepSeek: /fp-async – rồi mô tả công việc bạn muốn làm.
Nội dung SKILL.md
Nội dung SKILL.md gốc bằng tiếng Anh.
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](references/detailed-guide.md) 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/catchflows. - You want practical fp-ts async patterns instead of academic explanations.
// TaskEither<Error, User> means:
// "An async operation that either fails with Error or succeeds with User"---
5. Real API Examples
Complete Fetch Wrapper
// 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)
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)
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.
Skill liên quan
- fp-backend – Mẫu lập trình hàm cho phát triển backend Node.js/Deno với fp-ts, ReaderTaskEither và…
- fp-errors – Đừng throw ở mọi nơi - xử lý lỗi như giá trị bằng Either và TaskEither để code gọn gàng…
- fp-pragmatic – Hướng dẫn thực tế, không biệt ngữ về lập trình hàm - cách tiếp cận 80/20 mang lại kết…
- fp-react – Mẫu thực tế dùng fp-ts với React - hooks, state, form, lấy dữ liệu. Có ví dụ React theo…
- fp-refactor – Hướng dẫn toàn diện refactor code TypeScript theo kiểu mệnh lệnh sang mẫu fp-ts
- fp-taskeither-ref – Tài liệu tham khảo nhanh về TaskEither. Dùng khi cần xử lý lỗi bất đồng bộ, gọi API hoặc…
- fp-ts-errors – Xử lý lỗi như giá trị bằng fp-ts Either và TaskEither để code TypeScript gọn gàng và dễ…
- fp-ts-pragmatic – Hướng dẫn thực tế, không biệt ngữ về lập trình hàm với fp-ts - cách tiếp cận 80/20 mang…