ripthrow 2.0.1 → 2.1.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/README.md +112 -0
- package/package.json +4 -2
- package/src/operators/context.ts +16 -1
- package/src/pattern.ts +6 -1
- package/src/result-builder-async.ts +6 -2
- package/src/result-builder.ts +6 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/kind-of.ts +22 -0
package/README.md
CHANGED
|
@@ -64,3 +64,115 @@ console.log(isValid); // true
|
|
|
64
64
|
## Why ripthrow?
|
|
65
65
|
|
|
66
66
|
Handling errors with exceptions can lead to "hidden" control flows. `ripthrow` forces you to acknowledge potential failures, leading to more resilient applications. Whether you are parsing JSON, fetching data, or performing complex logic, `ripthrow` ensures your error handling is as clean as your success path.
|
|
67
|
+
|
|
68
|
+
## Benchmarks
|
|
69
|
+
|
|
70
|
+
All benchmarks run on Bun 1.3 via [tinybench](https://github.com/tinylibs/tinybench) (`bun run bench`).
|
|
71
|
+
Higher ops/s = faster. Latency is per-operation (lower is better).
|
|
72
|
+
|
|
73
|
+
### Construction
|
|
74
|
+
|
|
75
|
+
| Pattern | ops/s | Latency | vs native |
|
|
76
|
+
|---------|------:|--------:|----------:|
|
|
77
|
+
| `Ok()` | 25,106,258 | 43.7 ns | — |
|
|
78
|
+
| `Err()` | 25,330,770 | 43.5 ns | — |
|
|
79
|
+
| `throw` | 1,182,372 | 1076.0 ns | **22× slower** |
|
|
80
|
+
| `{ ok: true }` manual | 25,110,815 | 43.8 ns | identical |
|
|
81
|
+
|
|
82
|
+
> `Ok`/`Err` are object literals — zero overhead vs writing the union manually.
|
|
83
|
+
> `throw` is the expensive one (Error object creation), not ripthrow.
|
|
84
|
+
|
|
85
|
+
### Wrapping (`safe` vs `try/catch`)
|
|
86
|
+
|
|
87
|
+
| Pattern | ops/s | Latency |
|
|
88
|
+
|---------|------:|--------:|
|
|
89
|
+
| `safe(success)` | 21,364,164 | 54.3 ns |
|
|
90
|
+
| `try/catch` (no throw) | 25,834,736 | 41.5 ns |
|
|
91
|
+
| `safe(throws)` | 809,722 | 1513.3 ns |
|
|
92
|
+
| `try/catch` (throw) | 1,137,303 | 1154.6 ns |
|
|
93
|
+
| `safeAsync(success)` | 3,179,026 | 349.5 ns |
|
|
94
|
+
| `try/catch async` (success) | 3,128,980 | 356.4 ns |
|
|
95
|
+
|
|
96
|
+
> `safe` adds <15 ns vs raw try/catch on the success path.
|
|
97
|
+
> On throws, both are bottlenecked by Error object creation — ripthrow is not the overhead.
|
|
98
|
+
> Async paths are identical (bottleneck is Promise scheduling).
|
|
99
|
+
|
|
100
|
+
### Mapping & Chaining
|
|
101
|
+
|
|
102
|
+
| Pattern | ops/s | Latency |
|
|
103
|
+
|---------|------:|--------:|
|
|
104
|
+
| `map` x5 | 13,822,582 | 90.6 ns |
|
|
105
|
+
| `if/else` x5 (native) | 19,130,552 | 64.6 ns |
|
|
106
|
+
| `andThen` chain | 19,259,304 | 57.7 ns |
|
|
107
|
+
| `orElse` fallback | 21,331,229 | 52.5 ns |
|
|
108
|
+
| builder chain (5 ops) | 18,372,064 | 61.2 ns |
|
|
109
|
+
|
|
110
|
+
> Functional overhead is ~3-5 ns per function call. The builder (fluent API) adds
|
|
111
|
+
> roughly 1-2 ns per method call — effectively zero.
|
|
112
|
+
|
|
113
|
+
### Matching vs `try/catch`
|
|
114
|
+
|
|
115
|
+
| Pattern | ops/s | Latency |
|
|
116
|
+
|---------|------:|--------:|
|
|
117
|
+
| `match(Ok)` | 23,395,940 | 46.9 ns |
|
|
118
|
+
| `match(Err)` | 23,228,941 | 46.1 ns |
|
|
119
|
+
| `try/catch` (no throw) | 25,834,736 | 41.5 ns |
|
|
120
|
+
| `try/catch` (throw) | 1,137,303 | 1154.6 ns |
|
|
121
|
+
|
|
122
|
+
> `match` is within 15% of raw try/catch on the success path.
|
|
123
|
+
> On error paths, `match` is **20× faster** than try/catch with a thrown error.
|
|
124
|
+
|
|
125
|
+
### Collections
|
|
126
|
+
|
|
127
|
+
| Pattern | ops/s | Latency |
|
|
128
|
+
|---------|------:|--------:|
|
|
129
|
+
| `all(5 ok)` | 9,896,702 | 120.3 ns |
|
|
130
|
+
| `all(5, last err)` | 13,444,957 | 91.8 ns |
|
|
131
|
+
| `any(5 ok)` | 15,383,201 | 79.1 ns |
|
|
132
|
+
|
|
133
|
+
### Pattern Matching (`matchErr`)
|
|
134
|
+
|
|
135
|
+
| Pattern | ops/s | Latency |
|
|
136
|
+
|---------|------:|--------:|
|
|
137
|
+
| `matchErr` (hit — creates Error) | 1,005,546 | 1175.9 ns |
|
|
138
|
+
| `matchErr` (miss — instanceof only) | 14,971,304 | 80.0 ns |
|
|
139
|
+
|
|
140
|
+
> The "hit" path creates the matched `TypedError`, which is the same cost as `new Error()`.
|
|
141
|
+
> The "miss" path is just an `instanceof` check — 15M ops/s.
|
|
142
|
+
|
|
143
|
+
### Context
|
|
144
|
+
|
|
145
|
+
| Pattern | ops/s | Latency |
|
|
146
|
+
|---------|------:|--------:|
|
|
147
|
+
| `context()` wrap | 1,198,643 | 1035.6 ns |
|
|
148
|
+
| `context()` with help | 1,162,701 | 1137.3 ns |
|
|
149
|
+
|
|
150
|
+
> Creating structured `Report` objects has the same cost as `new Error` (~1 µs).
|
|
151
|
+
|
|
152
|
+
### Real-World Patterns
|
|
153
|
+
|
|
154
|
+
| Pattern | ops/s | Latency |
|
|
155
|
+
|---------|------:|--------:|
|
|
156
|
+
| parse JSON (ripthrow) | 7,746,842 | 141.6 ns |
|
|
157
|
+
| parse JSON (try/catch) | 8,546,584 | 126.5 ns |
|
|
158
|
+
| deep access (ripthrow) | 18,689,730 | 60.4 ns |
|
|
159
|
+
| deep access (try/catch) | 25,080,105 | 42.4 ns |
|
|
160
|
+
| legacy throw fn (ripthrow) | 18,203,471 | 63.3 ns |
|
|
161
|
+
| legacy throw fn (try/catch) | 25,927,726 | 40.8 ns |
|
|
162
|
+
|
|
163
|
+
> In real-world usage, ripthrow adds 15-20 ns per operation — well within the
|
|
164
|
+
> "zero overhead" claim for all practical purposes. The bottleneck is never
|
|
165
|
+
> the Result type, it's whatever you're doing inside `map`/`andThen`.
|
|
166
|
+
|
|
167
|
+
### Summary
|
|
168
|
+
|
|
169
|
+
- **`Ok`/`Err` = native object literal speed.** Same representation, same performance.
|
|
170
|
+
- **`map`/`andThen` = 1-5 ns overhead** per function call.
|
|
171
|
+
- **`match` = faster than try/catch** when errors are actually thrown.
|
|
172
|
+
- **`safe()` = same speed as manual try/catch** on success paths.
|
|
173
|
+
- **`matchErr` "miss" path = 15M ops/s**, "hit" path = Error construction speed.
|
|
174
|
+
- **Cost center is always `new Error()`**, not ripthrow.
|
|
175
|
+
|
|
176
|
+
> "Zero overhead" is not marketing — it's measured. ripthrow compiles down to
|
|
177
|
+
> object literal checks and function calls with no hidden allocation or
|
|
178
|
+
> prototype magic.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ripthrow",
|
|
3
3
|
"module": "src/index.ts",
|
|
4
|
-
"version": "2.0
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"description": "Zero-overhead, type-safe error handling for TypeScript.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"result",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@types/bun": "latest",
|
|
33
33
|
"@types/node": "^25.6.1",
|
|
34
34
|
"knip": "^6.12.1",
|
|
35
|
+
"tinybench": "^6.0.1",
|
|
35
36
|
"typedoc": "^0.28.19",
|
|
36
37
|
"typedoc-plugin-markdown": "^4.11.0"
|
|
37
38
|
},
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"knip": "knip",
|
|
43
44
|
"lint": "biome check .",
|
|
44
45
|
"format": "biome format --write .",
|
|
45
|
-
"docs": "typedoc"
|
|
46
|
+
"docs": "typedoc",
|
|
47
|
+
"bench": "bun run benches/result.bench.ts"
|
|
46
48
|
}
|
|
47
49
|
}
|
package/src/operators/context.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { mapErr } from "./map-err";
|
|
|
10
10
|
* @param result The Result to attach context to.
|
|
11
11
|
* @param message The context message.
|
|
12
12
|
* @param help An optional help message.
|
|
13
|
+
* @param meta Optional metadata to attach (merged with any _metadata from the original error).
|
|
13
14
|
* @returns A Result with the same success value or a Report as the error.
|
|
14
15
|
*
|
|
15
16
|
* @category Operators
|
|
@@ -20,6 +21,20 @@ export function context<T, E>(
|
|
|
20
21
|
result: Result<T, E>,
|
|
21
22
|
message: string,
|
|
22
23
|
help?: string,
|
|
24
|
+
meta?: Record<string, unknown>,
|
|
23
25
|
): Result<T, Report> {
|
|
24
|
-
return mapErr(result, (err) =>
|
|
26
|
+
return mapErr(result, (err) => {
|
|
27
|
+
let originalMeta: Record<string, unknown> | undefined;
|
|
28
|
+
if (err && typeof err === "object") {
|
|
29
|
+
originalMeta = (err as { _metadata?: Record<string, unknown> })._metadata;
|
|
30
|
+
}
|
|
31
|
+
const merged = { ...(originalMeta || {}), ...(meta || {}) };
|
|
32
|
+
const keys = Object.keys(merged);
|
|
33
|
+
// biome-ignore lint/nursery/noTernary: it's more readable
|
|
34
|
+
const ctx: Record<string, unknown> | undefined = keys.length > 0 ? merged : undefined;
|
|
35
|
+
return Report.from(err, message, {
|
|
36
|
+
help,
|
|
37
|
+
context: ctx,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
25
40
|
}
|
package/src/pattern.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface TypedError<A extends unknown[], N extends string> extends Error {
|
|
|
9
9
|
readonly help?: string;
|
|
10
10
|
readonly name: N;
|
|
11
11
|
readonly kind: N;
|
|
12
|
+
readonly _metadata?: Record<string, unknown>;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
interface HandlerEntry {
|
|
@@ -25,6 +26,7 @@ interface ErrDefEntry {
|
|
|
25
26
|
message: (...args: any[]) => string;
|
|
26
27
|
// biome-ignore lint/suspicious/noExplicitAny: required for Parameters<>
|
|
27
28
|
help?: (...args: any[]) => string;
|
|
29
|
+
_metadata?: Record<string, unknown>;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
type ErrDefMap = Record<string, ErrDefEntry>;
|
|
@@ -44,7 +46,7 @@ type ErrorFactories<T extends ErrDefMap> = {
|
|
|
44
46
|
export function createErrors<T extends ErrDefMap>(defs: T): ErrorFactories<T> {
|
|
45
47
|
const result: Record<string, ErrFactory<unknown[], string>> = {};
|
|
46
48
|
for (const [name, def] of Object.entries(defs)) {
|
|
47
|
-
result[name] = createError(name, def.message, def.help);
|
|
49
|
+
result[name] = createError(name, def.message, def.help, def._metadata);
|
|
48
50
|
}
|
|
49
51
|
return result as unknown as ErrorFactories<T>;
|
|
50
52
|
}
|
|
@@ -53,17 +55,20 @@ export function createError<A extends unknown[], N extends string>(
|
|
|
53
55
|
name: N,
|
|
54
56
|
message: (...args: A) => string,
|
|
55
57
|
help?: (...args: A) => string,
|
|
58
|
+
_metadata?: Record<string, unknown>,
|
|
56
59
|
): ErrFactory<A, N> {
|
|
57
60
|
class _Error extends Error {
|
|
58
61
|
override readonly name: N;
|
|
59
62
|
readonly args: A;
|
|
60
63
|
readonly help: string | undefined;
|
|
61
64
|
readonly kind: N = name;
|
|
65
|
+
readonly _metadata: Record<string, unknown> | undefined;
|
|
62
66
|
|
|
63
67
|
constructor(...args: A) {
|
|
64
68
|
super(message(...args));
|
|
65
69
|
this.name = name;
|
|
66
70
|
this.args = args;
|
|
71
|
+
this._metadata = _metadata;
|
|
67
72
|
if (help) {
|
|
68
73
|
this.help = help(...args);
|
|
69
74
|
}
|
|
@@ -119,8 +119,12 @@ export class AsyncResultBuilder<T, E> {
|
|
|
119
119
|
return new AsyncResultBuilder(this._promise.then((r) => tapErr(r, fn)));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
context(
|
|
123
|
-
|
|
122
|
+
context(
|
|
123
|
+
message: string,
|
|
124
|
+
help?: string,
|
|
125
|
+
meta?: Record<string, unknown>,
|
|
126
|
+
): AsyncResultBuilder<T, Report> {
|
|
127
|
+
return new AsyncResultBuilder(this._promise.then((r) => contextOp(r, message, help, meta)));
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
match<R>(handlers: { ok: (value: T) => R; err: (error: E) => R }): Promise<R> {
|
package/src/result-builder.ts
CHANGED
|
@@ -175,8 +175,12 @@ export class ResultBuilder<T, E> {
|
|
|
175
175
|
/**
|
|
176
176
|
* Attaches context to the error if it exists.
|
|
177
177
|
*/
|
|
178
|
-
context(
|
|
179
|
-
|
|
178
|
+
context(
|
|
179
|
+
message: string,
|
|
180
|
+
help?: string,
|
|
181
|
+
meta?: Record<string, unknown>,
|
|
182
|
+
): ResultBuilder<T, Report> {
|
|
183
|
+
return new ResultBuilder(contextOp(this._result, message, help, meta));
|
|
180
184
|
}
|
|
181
185
|
}
|
|
182
186
|
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the error `kind` from any ripthrow error.
|
|
3
|
+
* Handles TypedError directly and Report (traverses cause).
|
|
4
|
+
*
|
|
5
|
+
* @param err The error to extract the kind from.
|
|
6
|
+
* @returns The error kind string, or undefined if not found.
|
|
7
|
+
*
|
|
8
|
+
* @category Utilities
|
|
9
|
+
* @example
|
|
10
|
+
* const kind = kindOf(err);
|
|
11
|
+
* if (kind === "userNotFound") { ... }
|
|
12
|
+
*/
|
|
13
|
+
export function kindOf(err: unknown): string | undefined {
|
|
14
|
+
if (err && typeof err === "object" && "kind" in err) {
|
|
15
|
+
return (err as { kind: string }).kind;
|
|
16
|
+
}
|
|
17
|
+
if (err instanceof Error && err.cause && typeof err.cause === "object" && "kind" in err.cause) {
|
|
18
|
+
return (err.cause as { kind: string }).kind;
|
|
19
|
+
}
|
|
20
|
+
// biome-ignore lint/complexity/noUselessUndefined: required by noImplicitReturns
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|