Structured, composable execution for TypeScript with retry, timeout, cancellation, and task orchestration.
hardtry gives you a small fluent API for failure-aware execution. You can run sync/async work, map failures, compose retries and timeouts, orchestrate task maps, and early-exit flow pipelines.
import * as try$ from "hardtry"
const result = await try$
.retry(3)
.timeout(1_000)
.run({
try: async () => fetch("https://example.com"),
catch: () => new Error("request failed"),
})Table of Contents
- Immutable fluent builder (
retry,timeout,signal) - Top-level wrap builder (
wrap().wrap()) for terminal APIs - Sync and async execution (
runSync,run) - Typed failure mapping in object-form
run({ try, catch }) - Parallel task execution with dependency access via
this.$result - Flow orchestration with early-exit via
this.$exit(value) - Resource cleanup with
AsyncDisposableStack - Typed generator composition through
gen
# bun
bun add hardtry
# npm
npm install hardtry
# pnpm
pnpm add hardtry
# yarn
yarn add hardtry| Term | Meaning |
|---|---|
run |
Async entrypoint that returns Promise<value | error> |
runSync |
Sync entrypoint for sync-only execution |
retry(limit) |
Retry policy, where limit includes the first attempt |
timeout(ms) |
Total execution timeout (attempts + delays + catch) |
signal(abortSignal) |
External cancellation integration |
wrap(fn) |
Top-level middleware builder for run, runSync, all, flow, and gen |
all(tasks) |
Fail-fast parallel named tasks |
settled().all(tasks) |
Settled parallel named tasks |
flow(tasks) |
Task orchestration with early exit |
import * as try$ from "hardtry"
const value = await try$.run({
try: async () => {
return "ok"
},
catch: () => "mapped-error",
})
// value: "ok" | "mapped-error"Function form maps thrown errors to UnhandledException.
const syncValue = try$.runSync(() => 42)
const asyncValue = await try$.run(async () => 42)Object form lets you map failures with catch.
const result = await try$.run({
try: async () => {
throw new Error("boom")
},
catch: () => "fallback" as const,
})
// "fallback"const controller = new AbortController()
const result = await try$
.retry({ backoff: "constant", delayMs: 50, limit: 3 })
.timeout(1_000)
.signal(controller.signal)
.run(async (ctx) => {
return `attempt-${ctx.retry.attempt}`
})
const wrapped = await try$.wrap((ctx, next) => next(ctx)).run(async () => "ok")
// wrap is top-level only
// valid: try$.wrap(w1).wrap(w2).all(...)
// invalid: try$.retry(3).wrap(w1)Fail-fast parallel tasks:
const values = await try$.all({
a() {
return 1
},
async b() {
const a = await this.$result.a
return a + 1
},
})
// { a: 1, b: 2 }Settled mode:
const settled = await try$.settled().all({
fail() {
throw new Error("boom")
},
ok() {
return 1
},
})flow is ideal for dependent pipeline steps where you may short-circuit early.
Cache hit (early exit in task a):
const cacheHit = await try$.flow({
a() {
const cached: string | null = "cached-value"
if (cached !== null) {
return this.$exit(cached)
}
return null
},
async b() {
return "api-value"
},
async c() {
const apiValue = await this.$result.b
return this.$exit(`${apiValue}-transformed`)
},
})
// "cached-value"Cache miss (continue to API + transform):
const cacheMiss = await try$.flow({
a() {
const cached: string | null = null
if (cached !== null) {
return this.$exit(cached)
}
return null
},
async b() {
return "api-value"
},
async c() {
const apiValue = await this.$result.b
return this.$exit(`${apiValue}-transformed`)
},
})
// "api-value-transformed"const value = await try$.gen(function* (use) {
const a = yield* use(try$.run(() => 1))
const b = yield* use(try$.run(() => a + 1))
return b
})await using disposer = try$.dispose()
disposer.defer(() => {
// cleanup
})retrysettledtimeoutsignalwraprunrunSyncallflowdisposegencreateRetryPolicyCancellationErrorTimeoutErrorRetryExhaustedErrorUnhandledExceptionPanic
run(tryFn)->Promise<T | UnhandledException | ConfigErrors>run({ try, catch })->Promise<T | C | ConfigErrors>runSync(tryFn)->T | UnhandledExceptionall(tasks)->Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>> }>settled().all(tasks)-> settled result mapflow(tasks)->Promise<FlowExitUnion>
- Retry
limitincludes the first attempt. - Timeout scope is total execution.
flowrequires at least one$exit(...)path; otherwise it throws.- Control outcomes have precedence over mapped catch results in racing scenarios.
wrapis only available fromtry$.wrap(...)and can be chained as.wrap().wrap().- Programmer-error paths throw
Panic, not a returned error value. Panicexposes acodefor machine-readable diagnostics.
WRAP_UNAVAILABLEWRAP_INVALID_HANDLERRUN_SYNC_UNAVAILABLERUN_SYNC_INVALID_INPUTFLOW_NO_EXITGEN_UNAVAILABLEGEN_INVALID_FACTORYRUN_SYNC_WRAPPED_RESULT_PROMISERUN_SYNC_TRY_PROMISERUN_SYNC_CATCH_PROMISERUN_SYNC_ASYNC_RETRY_POLICYRUN_CATCH_HANDLER_THROWRUN_CATCH_HANDLER_REJECTRUN_SYNC_CATCH_HANDLER_THROWALL_CATCH_HANDLER_THROWALL_CATCH_HANDLER_REJECTTASK_INVALID_HANDLERTASK_SELF_REFERENCETASK_UNKNOWN_REFERENCEUNREACHABLE_RETRY_POLICY_BACKOFF
Contributions are welcome. Please run:
bun run format
bun run check
bun run typecheck
bun run testMade with pastry