The problem
Most resilience libraries in the TypeScript ecosystem are tied to HTTP clients (axios-retry, p-retry, Polly.js) or require wrapping your function in a class with a .execute() ceremony. What if you just want to wrap any async function — a database query, an internal service call, a file operation — with retry logic, a timeout, and a circuit breaker, without pulling in heavy dependencies?
Enter houhou.
What is houhou?
Houhou is a zero-dependency TypeScript library (~500 LOC) that wraps any async function with composable resilience policies. The wrapped function keeps the exact same signature — you call it like the original.
import { task } from 'houhou'
const charge = task(chargeCard)
.retry(3)
.timeout(10_000)
.fallback(() => ({ status: 'pending' }))
await charge(account, amount)
Enter fullscreen mode Exit fullscreen mode
Policies at a glance
Retry
Re-execute on failure with fixed or exponential backoff:
task(fetchUser).retry(3) // shorthand
task(fetchUser).retry({
attempts: 5,
backoff: 'exponential',
jitter: true,
delay: 500
})
Enter fullscreen mode Exit fullscreen mode
Timeout
Reject if the function doesn't complete in time:
task(fetchUser).timeout(5000)
Enter fullscreen mode Exit fullscreen mode
Fallback
Run an alternative function on failure:
task(fetchUser).fallback(() => loadFromCache(id))
Enter fullscreen mode Exit fullscreen mode
Circuit Breaker
Prevent repeated calls to an unhealthy service:
task(queryDb).circuitBreaker({
failureThreshold: 5,
successThreshold: 2,
resetTimeout: 30_000
})
Enter fullscreen mode Exit fullscreen mode
Delay
Wait before execution:
task(syncData).delay(1000)
Enter fullscreen mode Exit fullscreen mode
Policy ordering matters
Policies are nested: the last method called wraps the previous ones. Execution order is reverse of declaration order.
task(fn).retry(3).timeout(1000)
// → timeout wraps retry
// → function runs → retry on failure (up to 3 times) → 1s total timeout
// → if the timeout fires, there are no more retries
Enter fullscreen mode Exit fullscreen mode
task(fn).timeout(1000).retry(3)
// → retry wraps timeout
// → function runs → 1s timeout → if timeout fires, retry catches it
// → the whole cycle repeats up to 3 times
Enter fullscreen mode Exit fullscreen mode
Type safety — policy lock
Each method is lockable. TypeScript prevents configuring the same policy twice at compile time, and a runtime Set guard enforces it at runtime:
const t = task(fn).retry(3)
// @ts-expect-error — 'retry' is already locked
t.retry(2) // throws at runtime too
Enter fullscreen mode Exit fullscreen mode
Cancellation with AbortSignal
Houhou uses AbortController to cancel operations when a timeout fires or an external signal is provided. The AbortSignal is passed as the last argument to your function:
const fn = (url: string, signal?: AbortSignal) => fetch(url, { signal })
task(fn).timeout(5000)('https://api.example.com')
// → timeout fires → controller.abort() → fetch is cancelled
Enter fullscreen mode Exit fullscreen mode
You can also pass your own external signal:
const controller = new AbortController()
const promise = task(fn).timeout(5000).retry(3)('url', controller.signal)
controller.abort() // cancels everything
Enter fullscreen mode Exit fullscreen mode
Composition example
Policies chain fluently in any order:
const resilient = task(callApi)
.retry({ attempts: 3, backoff: 'exponential' })
.timeout(5000)
.fallback(loadFromCache)
.circuitBreaker({ failureThreshold: 5, successThreshold: 2, resetTimeout: 30_000 })
.delay(100)
Enter fullscreen mode Exit fullscreen mode
The name
"houhou" means "method" or "way of doing" in Japanese — fitting for a library about how you execute your functions.
Try it
npm install houhou
# or
pnpm add houhou
yarn add houhou
Enter fullscreen mode Exit fullscreen mode
GitHub: https://github.com/smokeeaasd/houhou
npm: https://npmjs.com/package/houhou
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.