tryharder 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Adel Rodriguez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,468 @@
1
+ <div align="center">
2
+ <h1 align="center">🔁 tryharder</h1>
3
+
4
+ <p align="center">
5
+ <strong>A better try/catch for TypeScript</strong>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/tryharder"><img src="https://img.shields.io/npm/v/tryharder" alt="npm version" /></a>
10
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
11
+ </p>
12
+ </div>
13
+
14
+ Run sync and async work with retries, timeouts, cancellation, typed failure mapping, and orchestration. Use `tryharder` when plain `try/catch` starts to sprawl and you want a small execution API that scales from single calls to parallel task maps and early-exit pipelines.
15
+
16
+ ```ts
17
+ import * as try$ from "tryharder"
18
+
19
+ class RequestFailedError extends Error {}
20
+
21
+ const result = await try$
22
+ .retry(3) // Retry up to 3 times
23
+ .timeout(5_000) // Timeout after 5 seconds
24
+ .run({
25
+ try: async () => {
26
+ const response = await fetch("https://example.com")
27
+
28
+ if (!response.ok) {
29
+ throw new Error(`request failed: ${response.status}`)
30
+ }
31
+
32
+ return "ok" as const
33
+ },
34
+ catch: () => new RequestFailedError("request failed"),
35
+ })
36
+
37
+ // result is "ok" | RequestFailedError | RetryExhaustedError | TimeoutError
38
+ ```
39
+
40
+ <details>
41
+ <summary>Table of Contents</summary>
42
+
43
+ - [Features](#features)
44
+ - [Installation](#installation)
45
+ - [Migration from hardtry](#migration-from-hardtry)
46
+ - [Core Concepts](#core-concepts)
47
+ - [Quick Start](#quick-start)
48
+ - [Usage](#usage)
49
+ - [run and runSync](#run-and-runsync)
50
+ - [retry, timeout, signal](#retry-timeout-signal)
51
+ - [wrap](#wrap)
52
+ - [all and allSettled](#all-and-allsettled)
53
+ - [flow and $exit](#flow-and-exit)
54
+ - [gen](#gen)
55
+ - [dispose](#dispose)
56
+ - [API Reference](#api-reference)
57
+ - [Common Recipes](#common-recipes)
58
+ - [When not to use tryharder](#when-not-to-use-tryharder)
59
+ - [Contributing](#contributing)
60
+ - [Acknowledgments](#acknowledgments)
61
+ - [License](#license)
62
+
63
+ </details>
64
+
65
+ ## Features
66
+
67
+ - **Composable execution policies** - Chain `retry`, `timeout`, and `signal` around work that may fail or be cancelled.
68
+ - **Typed failure mapping** - Use object-form `run({ try, catch })` to map thrown errors into domain-specific results.
69
+ - **Sync and async entrypoints** - Use `runSync` for synchronous work and `run` for asynchronous work.
70
+ - **Task orchestration** - Coordinate named task maps with `all`, `allSettled`, and `flow`.
71
+ - **Early exit control** - Short-circuit pipelines with `this.$exit(...)` in `flow`.
72
+ - **Middleware-style wrapping** - Observe execution with top-level `wrap(...).wrap(...)` chains.
73
+ - **Generator composition** - Build linear workflows over `run(...)` results with `gen(...)`.
74
+ - **Resource cleanup** - Register cleanup with `dispose()` and `AsyncDisposableStack`.
75
+ - **No runtime dependencies** - The published package ships without runtime dependencies.
76
+
77
+ ## Installation
78
+
79
+ ```bash
80
+ # bun
81
+ bun add tryharder
82
+
83
+ # npm
84
+ npm install tryharder
85
+
86
+ # yarn
87
+ yarn add tryharder
88
+
89
+ # pnpm
90
+ pnpm add tryharder
91
+ ```
92
+
93
+ ## Migration from hardtry
94
+
95
+ Replace import specifiers only:
96
+
97
+ - `hardtry` -> `tryharder`
98
+ - `hardtry/errors` -> `tryharder/errors`
99
+ - `hardtry/types` -> `tryharder/types`
100
+
101
+ You can keep the same namespace alias in your code:
102
+
103
+ ```ts
104
+ import * as try$ from "tryharder"
105
+ ```
106
+
107
+ No runtime API names changed.
108
+
109
+ ## Core Concepts
110
+
111
+ | Term | Meaning |
112
+ | --------------------- | ------------------------------------------------------------------------- |
113
+ | `run` | Async entrypoint that returns a value, a mapped failure, or config error |
114
+ | `runSync` | Sync entrypoint for synchronous work only |
115
+ | `retry(limit)` | Retry policy where `limit` includes the first attempt |
116
+ | `timeout(ms)` | Total execution timeout across attempts, delays, and catch handling |
117
+ | `signal(abortSignal)` | External cancellation for `run` and, from the root builder, orchestration |
118
+ | `wrap(fn)` | Top-level middleware hook around terminal execution APIs |
119
+ | `all(tasks)` | Fail-fast parallel named tasks |
120
+ | `allSettled(tasks)` | Settled parallel named tasks |
121
+ | `flow(tasks)` | Ordered task orchestration with early exit |
122
+ | `$exit(value)` | Stop a `flow` early and return `value` |
123
+
124
+ Not sure if `tryharder` is a good fit for your project? See [When not to use tryharder](#when-not-to-use-tryharder).
125
+
126
+ ## Quick Start
127
+
128
+ Use function form when you want thrown failures normalized to `UnhandledException`:
129
+
130
+ ```ts
131
+ import * as try$ from "tryharder"
132
+
133
+ const result = await try$.run(async () => {
134
+ return "ok" as const
135
+ })
136
+
137
+ // "ok" | UnhandledException
138
+ ```
139
+
140
+ Use object form when you want to map failures into domain results:
141
+
142
+ ```ts
143
+ import * as try$ from "tryharder"
144
+
145
+ class ValidationError extends Error {}
146
+
147
+ const result = await try$.run({
148
+ try: async () => {
149
+ throw new Error("boom")
150
+ },
151
+ catch: () => new ValidationError("invalid input"),
152
+ })
153
+
154
+ // ValidationError
155
+ ```
156
+
157
+ In a real application, you usually compose policies before the terminal call:
158
+
159
+ ```ts
160
+ class UpstreamUnavailableError extends Error {}
161
+
162
+ const result = await try$
163
+ .retry({ backoff: "constant", delayMs: 100, limit: 3 })
164
+ .timeout(1_500)
165
+ .run({
166
+ try: async () => {
167
+ const response = await fetch("https://example.com/data")
168
+
169
+ if (!response.ok) {
170
+ throw new Error("upstream failed")
171
+ }
172
+
173
+ return await response.json()
174
+ },
175
+ catch: () => new UpstreamUnavailableError("data service unavailable"),
176
+ })
177
+ ```
178
+
179
+ ## Usage
180
+
181
+ ### run and runSync
182
+
183
+ Use function form when you want thrown failures wrapped as `UnhandledException`.
184
+
185
+ ```ts
186
+ const syncValue = try$.runSync(() => 42)
187
+
188
+ const asyncValue = await try$.run(async () => {
189
+ return 42
190
+ })
191
+ ```
192
+
193
+ Use object form when you want to map failures yourself.
194
+
195
+ ```ts
196
+ class InvalidInputError extends Error {}
197
+ class PermissionDeniedError extends Error {}
198
+
199
+ const result = try$.runSync({
200
+ try: () => {
201
+ throw new SyntaxError("bad input")
202
+ },
203
+ catch: (error) => {
204
+ if (error instanceof SyntaxError) {
205
+ return new InvalidInputError("invalid")
206
+ }
207
+
208
+ return new PermissionDeniedError("denied")
209
+ },
210
+ })
211
+ ```
212
+
213
+ ### retry, timeout, signal
214
+
215
+ Use modifiers to add retry, total timeout, and cancellation around `run(...)`. `signal(...)` can also be applied at the root builder before `all`, `allSettled`, or `flow`.
216
+
217
+ ```ts
218
+ const controller = new AbortController()
219
+
220
+ const result = await try$
221
+ .retry({ backoff: "constant", delayMs: 50, limit: 3 })
222
+ .timeout(1_000)
223
+ .signal(controller.signal)
224
+ .run(async (ctx) => {
225
+ return `attempt-${ctx.retry.attempt}`
226
+ })
227
+ ```
228
+
229
+ `timeout(ms)` measures total execution time, not just a single attempt.
230
+
231
+ ### wrap
232
+
233
+ Use `wrap(...)` for observational middleware around terminal APIs. Wraps are top-level only and can be chained as `.wrap().wrap()`.
234
+
235
+ ```ts
236
+ const result = await try$
237
+ .wrap((ctx, next) => {
238
+ console.log("starting attempt", ctx.retry.attempt)
239
+ return next()
240
+ })
241
+ .wrap((_ctx, next) => next())
242
+ .run(async () => "ok")
243
+ ```
244
+
245
+ `wrap(...)` is not available after `retry(...)`, `timeout(...)`, or `signal(...)`.
246
+
247
+ ### all and allSettled
248
+
249
+ Use `all(...)` for fail-fast task maps and `allSettled(...)` when you want every task result.
250
+
251
+ ```ts
252
+ const values = await try$.all({
253
+ a() {
254
+ return 1
255
+ },
256
+ async b() {
257
+ const a = await this.$result.a
258
+ return a + 1
259
+ },
260
+ })
261
+
262
+ // { a: 1, b: 2 }
263
+ ```
264
+
265
+ ```ts
266
+ const settled = await try$.allSettled({
267
+ fail() {
268
+ throw new Error("boom")
269
+ },
270
+ ok() {
271
+ return 1
272
+ },
273
+ })
274
+ ```
275
+
276
+ ### flow and $exit
277
+
278
+ Use `flow(...)` for dependent pipelines that may short-circuit early. At least one path must call `this.$exit(...)`.
279
+
280
+ ```ts
281
+ const result = await try$.flow({
282
+ cache() {
283
+ const cached: string | null = "cached-value"
284
+
285
+ if (cached !== null) {
286
+ return this.$exit(cached)
287
+ }
288
+
289
+ return null
290
+ },
291
+ async api() {
292
+ return "api-value"
293
+ },
294
+ async transform() {
295
+ const value = await this.$result.api
296
+ return this.$exit(`${value}-transformed`)
297
+ },
298
+ })
299
+ ```
300
+
301
+ ### gen
302
+
303
+ Use `gen(...)` when you want a more linear style over `run(...)` results.
304
+
305
+ ```ts
306
+ const value = await try$.gen(function* (use) {
307
+ const a = yield* use(try$.run(() => 1))
308
+ const b = yield* use(try$.run(() => a + 1))
309
+ return b
310
+ })
311
+ ```
312
+
313
+ ### dispose
314
+
315
+ Use `dispose()` to register cleanup for work that spans async boundaries.
316
+
317
+ ```ts
318
+ await using disposer = try$.dispose()
319
+
320
+ disposer.defer(() => {
321
+ console.log("cleanup")
322
+ })
323
+ ```
324
+
325
+ ## API Reference
326
+
327
+ ### Runtime
328
+
329
+ | Export | Description |
330
+ | -------------- | ------------------------------------------- |
331
+ | `run` | Async execution entrypoint |
332
+ | `runSync` | Sync execution entrypoint |
333
+ | `retry` | Create a retry policy builder |
334
+ | `retryOptions` | Normalize retry policy input |
335
+ | `timeout` | Add a total execution timeout |
336
+ | `signal` | Add external cancellation |
337
+ | `wrap` | Add top-level middleware hooks |
338
+ | `all` | Run fail-fast parallel named tasks |
339
+ | `allSettled` | Run settled parallel named tasks |
340
+ | `flow` | Run ordered tasks with early exit |
341
+ | `gen` | Compose `run(...)` calls through generators |
342
+ | `dispose` | Create an `AsyncDisposableStack` helper |
343
+
344
+ ### Errors
345
+
346
+ Exports from `tryharder/errors`:
347
+
348
+ | Export | Description |
349
+ | --------------------- | --------------------------------------------------------- |
350
+ | `CancellationError` | Returned or thrown when execution is externally cancelled |
351
+ | `TimeoutError` | Returned when timed execution expires |
352
+ | `RetryExhaustedError` | Returned when retry attempts are exhausted |
353
+ | `UnhandledException` | Returned when function-form execution throws |
354
+ | `Panic` | Thrown for programmer errors and invalid API usage |
355
+
356
+ ### Types
357
+
358
+ Exports from `tryharder/types`:
359
+
360
+ | Export | Description |
361
+ | ------------------ | ---------------------------------------------------- |
362
+ | `AllSettledResult` | Settled result map returned by `allSettled(...)` |
363
+ | `SettledFulfilled` | Fulfilled branch of a settled task result |
364
+ | `SettledRejected` | Rejected branch of a settled task result |
365
+ | `SettledResult` | Union of fulfilled and rejected settled task results |
366
+ | `FlowExit` | Exit marker type used by `flow(...)` |
367
+
368
+ ```ts
369
+ import * as try$ from "tryharder"
370
+ import { Panic, TimeoutError, UnhandledException } from "tryharder/errors"
371
+ import type { FlowExit, SettledResult } from "tryharder/types"
372
+ ```
373
+
374
+ ## Common Recipes
375
+
376
+ ### Retry a flaky request with timeout
377
+
378
+ ```ts
379
+ const result = await try$
380
+ .retry({ backoff: "constant", delayMs: 200, limit: 3 })
381
+ .timeout(2_000)
382
+ .run(async () => {
383
+ const response = await fetch("https://example.com")
384
+ return response.text()
385
+ })
386
+ ```
387
+
388
+ ### Map thrown exceptions into domain results
389
+
390
+ ```ts
391
+ class RemoteServiceError extends Error {}
392
+
393
+ const result = await try$.run({
394
+ try: async () => {
395
+ throw new Error("boom")
396
+ },
397
+ catch: () => new RemoteServiceError("service failed"),
398
+ })
399
+ ```
400
+
401
+ ### Run dependent parallel tasks with all
402
+
403
+ ```ts
404
+ const result = await try$.all({
405
+ async user() {
406
+ return { id: "1", name: "Ada" }
407
+ },
408
+ async profile() {
409
+ const user = await this.$result.user
410
+ return { userId: user.id, plan: "pro" as const }
411
+ },
412
+ })
413
+ ```
414
+
415
+ ### Short-circuit a pipeline with flow
416
+
417
+ ```ts
418
+ const result = await try$.flow({
419
+ cache() {
420
+ return this.$exit("cached" as const)
421
+ },
422
+ async api() {
423
+ return "remote"
424
+ },
425
+ })
426
+ ```
427
+
428
+ ### Add per-task retry inside orchestration
429
+
430
+ `retry(...)` and `timeout(...)` apply to `run(...)` and `runSync(...)`, not directly to `all(...)` or `flow(...)`. When you need per-task policies, wrap leaf work in nested `run(...)` calls.
431
+
432
+ ```ts
433
+ const result = await try$.flow({
434
+ async fetchUser() {
435
+ const user = await try$.retry(2).run(async () => {
436
+ const response = await fetch("https://example.com/user")
437
+ return await response.json()
438
+ })
439
+
440
+ return this.$exit(user)
441
+ },
442
+ })
443
+ ```
444
+
445
+ ## When not to use tryharder
446
+
447
+ - **Small scripts or one-off tasks** - Plain `try/catch` is often simpler when you do not need retries, cancellation, or orchestration.
448
+ - **You already use an effect system or result abstraction** - If your codebase already has a consistent execution model, adding `tryharder` may be redundant.
449
+ - **Your workflows are mostly straightforward Promise chains** - `all(...)` and `flow(...)` help when coordination matters; otherwise native composition may be clearer.
450
+ - **Your team prefers explicit `Result` values everywhere** - `tryharder` centers execution wrappers, not a dedicated result data type.
451
+ - **You do not want policy-driven execution behavior** - If retry and timeout semantics are unnecessary overhead, the abstraction may not pay for itself.
452
+
453
+ ## Contributing
454
+
455
+ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, code quality requirements, testing expectations, and changeset guidance.
456
+
457
+ ## Acknowledgments
458
+
459
+ - [`better-result`](https://github.com/dmmulroy/better-result) for typed result-oriented error handling in TypeScript.
460
+ - [`effect`](https://github.com/Effect-TS/effect) for structured, composable models of execution, failure, and concurrency.
461
+ - [`better-all`](https://github.com/shuding/better-all) for task orchestration patterns over object-shaped work graphs.
462
+ - [`errore`](https://errore.org/) for modeling errors as unions instead of tuples.
463
+
464
+ Made with [🥐 `pastry`](https://github.com/adelrodriguez/pastry)
465
+
466
+ ## License
467
+
468
+ [MIT](LICENSE)
@@ -0,0 +1,22 @@
1
+ type PanicCode = "ALL_CATCH_HANDLER_REJECT" | "ALL_CATCH_HANDLER_THROW" | "FLOW_NO_EXIT" | "ORCHESTRATION_UNSUPPORTED_POLICY" | "RETRY_INVALID_LIMIT" | "RUN_CATCH_HANDLER_REJECT" | "RUN_CATCH_HANDLER_THROW" | "RUN_SYNC_ASYNC_RETRY_POLICY" | "RUN_SYNC_CATCH_HANDLER_THROW" | "RUN_SYNC_CATCH_PROMISE" | "RUN_SYNC_TRY_PROMISE" | "RUN_SYNC_WRAPPED_RESULT_PROMISE" | "TASK_INVALID_HANDLER" | "TASK_SELF_REFERENCE" | "TIMEOUT_INVALID_MS" | "TASK_UNKNOWN_REFERENCE" | "UNREACHABLE_RETRY_POLICY_BACKOFF";
2
+ /** @internal */
3
+ declare class ControlError extends Error {}
4
+ declare class CancellationError extends ControlError {
5
+ constructor(message?: string, options?: ErrorOptions);
6
+ }
7
+ declare class TimeoutError extends ControlError {
8
+ constructor(message?: string, options?: ErrorOptions);
9
+ }
10
+ declare class RetryExhaustedError extends Error {
11
+ constructor(message?: string, options?: ErrorOptions);
12
+ }
13
+ declare class UnhandledException extends Error {
14
+ constructor(message?: string, options?: ErrorOptions);
15
+ }
16
+ declare class Panic extends Error {
17
+ readonly code: PanicCode;
18
+ constructor(code: PanicCode, options?: ErrorOptions & {
19
+ message?: string;
20
+ });
21
+ }
22
+ export { UnhandledException, TimeoutError, RetryExhaustedError, PanicCode, Panic, CancellationError };
package/dist/errors.js ADDED
@@ -0,0 +1,17 @@
1
+ import {
2
+ CancellationError,
3
+ Panic,
4
+ RetryExhaustedError,
5
+ TimeoutError,
6
+ UnhandledException
7
+ } from "./shared/chunk-996eswzx.js";
8
+ export {
9
+ UnhandledException,
10
+ TimeoutError,
11
+ RetryExhaustedError,
12
+ Panic,
13
+ CancellationError
14
+ };
15
+
16
+ //# debugId=7909B944ED0605C464756E2164756E21
17
+ //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFtdLAogICJzb3VyY2VzQ29udGVudCI6IFsKICBdLAogICJtYXBwaW5ncyI6ICIiLAogICJkZWJ1Z0lkIjogIjc5MDlCOTQ0RUQwNjA1QzQ2NDc1NkUyMTY0NzU2RTIxIiwKICAibmFtZXMiOiBbXQp9