wellcrafted 0.17.0 β 0.19.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 +354 -727
- package/dist/error/index.d.ts +70 -2
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +51 -1
- package/dist/error/index.js.map +1 -1
- package/dist/index-DRbikk8-.d.ts +28 -0
- package/dist/index-DRbikk8-.d.ts.map +1 -0
- package/dist/query/index.d.ts +119 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +237 -0
- package/dist/query/index.js.map +1 -0
- package/dist/result/index.d.ts +3 -455
- package/dist/result/index.js +3 -360
- package/dist/result-BCv06xhR.js +30 -0
- package/dist/result-BCv06xhR.js.map +1 -0
- package/dist/result-DxmXBFi5.js +335 -0
- package/dist/result-DxmXBFi5.js.map +1 -0
- package/dist/result-t0dngyiE.d.ts +431 -0
- package/dist/result-t0dngyiE.d.ts.map +1 -0
- package/package.json +7 -2
- package/dist/result/index.d.ts.map +0 -1
- package/dist/result/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,125 +7,60 @@
|
|
|
7
7
|
|
|
8
8
|
*Delightful TypeScript utilities for elegant, type-safe applications*
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Transform unpredictable errors into type-safe results
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- [Understanding TaggedError](#understanding-taggederror)
|
|
19
|
-
- [Basic Usage](#basic-usage)
|
|
20
|
-
- [Wrapping Functions That Throw](#wrapping-functions-that-throw)
|
|
21
|
-
- [API Reference](#api-reference)
|
|
22
|
-
- [Design Philosophy](#design-philosophy)
|
|
23
|
-
- [FAQ](#faq)
|
|
24
|
-
|
|
25
|
-
## Quick Start
|
|
26
|
-
|
|
27
|
-
**30-second example:** Transform throwing async operations into type-safe Results.
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
npm install wellcrafted
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
import { tryAsync } from "wellcrafted/result";
|
|
35
|
-
import { type TaggedError } from "wellcrafted/error";
|
|
36
|
-
import * as fs from 'fs/promises';
|
|
37
|
-
|
|
38
|
-
type FileError = TaggedError<"FileError">;
|
|
39
|
-
|
|
40
|
-
async function readConfig(path: string) {
|
|
41
|
-
return tryAsync<string, FileError>({
|
|
42
|
-
try: async () => {
|
|
43
|
-
const content = await fs.readFile(path, 'utf-8');
|
|
44
|
-
return content;
|
|
45
|
-
},
|
|
46
|
-
mapError: (error) => ({
|
|
47
|
-
name: "FileError",
|
|
48
|
-
message: "Failed to read configuration file",
|
|
49
|
-
context: { path },
|
|
50
|
-
cause: error
|
|
51
|
-
})
|
|
52
|
-
});
|
|
12
|
+
```typescript
|
|
13
|
+
// β Before: Which errors can this throw? π€·
|
|
14
|
+
try {
|
|
15
|
+
await saveUser(user);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
// ... good luck debugging in production
|
|
53
18
|
}
|
|
54
19
|
|
|
55
|
-
//
|
|
56
|
-
const { data, error } = await
|
|
57
|
-
|
|
20
|
+
// β
After: Every error is visible and typed
|
|
21
|
+
const { data, error } = await saveUser(user);
|
|
58
22
|
if (error) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
23
|
+
switch (error.name) {
|
|
24
|
+
case "ValidationError":
|
|
25
|
+
showToast(`Invalid ${error.context.field}`);
|
|
26
|
+
break;
|
|
27
|
+
case "AuthError":
|
|
28
|
+
redirectToLogin();
|
|
29
|
+
break;
|
|
30
|
+
// TypeScript ensures you handle all cases!
|
|
31
|
+
}
|
|
62
32
|
}
|
|
63
|
-
|
|
64
|
-
console.log("Config loaded:", data); // TypeScript knows data is safe here
|
|
65
33
|
```
|
|
66
34
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## Core Idea: The Result Type
|
|
72
|
-
|
|
73
|
-
> **π‘ TL;DR:** Replace `throw new Error()` with `return Err()` to make errors visible in your function signatures.
|
|
74
|
-
|
|
75
|
-
JavaScript's traditional error handling, based on `try...catch` and throwing `Error` objects, has two major drawbacks for modern application development:
|
|
76
|
-
1. **It's not type-safe**: A function signature `function doSomething(): User` doesn't tell you that it might throw a `NetworkError` or a `ValidationError`. Errors are invisible until they strike at runtime.
|
|
77
|
-
2. **It's not serialization-friendly**: `Error` instances lose their prototype chain when crossing serialization boundaries (JSON.stringify/parse, network requests, worker threads), breaking `instanceof` checks.
|
|
78
|
-
|
|
79
|
-
This library solves these problems with the `Result<T, E>` type. Instead of throwing, functions return a `Result` object that explicitly represents either a success or a failure.
|
|
80
|
-
|
|
81
|
-
A `Result` is a union of two "variants":
|
|
82
|
-
- **`Ok<T>`**: Represents a successful outcome, containing a `data` field with the success value. In this variant, the `error` property is always `null`.
|
|
83
|
-
- **`Err<E>`**: Represents a failure outcome, containing an `error` field with the error value. In this variant, the `data` property is always `null`.
|
|
84
|
-
|
|
85
|
-
This structure allows TypeScript's control-flow analysis to act as if it's a **discriminated union**. By checking if `result.error === null`, TypeScript knows it must be an `Ok` variant and can safely access `result.data`. This makes error handling explicit, type-safe, and predictable.
|
|
35
|
+
## A collection of simple, powerful primitives
|
|
86
36
|
|
|
87
|
-
###
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
export type Err<E> = { error: E; data: null };
|
|
95
|
-
|
|
96
|
-
// Result is just a union of these two types
|
|
97
|
-
export type Result<T, E> = Ok<T> | Err<E>;
|
|
98
|
-
|
|
99
|
-
// Helper functions to create each variant
|
|
100
|
-
export const Ok = <T>(data: T): Ok<T> => ({ data, error: null });
|
|
101
|
-
export const Err = <E>(error: E): Err<E> => ({ error, data: null });
|
|
37
|
+
### π― Result Type
|
|
38
|
+
Make errors explicit in function signatures
|
|
39
|
+
```typescript
|
|
40
|
+
function divide(a: number, b: number): Result<number, string> {
|
|
41
|
+
if (b === 0) return Err("Division by zero");
|
|
42
|
+
return Ok(a / b);
|
|
43
|
+
}
|
|
102
44
|
```
|
|
103
45
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
This design creates a **discriminated union** where the `error` (or `data`) property acts as the discriminant (with literal types `null` vs non-null), allowing TypeScript to automatically narrow types:
|
|
46
|
+
### π·οΈ Brand Types
|
|
47
|
+
Create distinct types from primitives
|
|
48
|
+
```typescript
|
|
49
|
+
type UserId = Brand<string, "UserId">;
|
|
50
|
+
type OrderId = Brand<string, "OrderId">;
|
|
111
51
|
|
|
112
|
-
|
|
113
|
-
function
|
|
114
|
-
if (result.error === null) {
|
|
115
|
-
// TypeScript knows this is Ok<T>
|
|
116
|
-
console.log(result.data); // β
data is type T
|
|
117
|
-
// console.log(result.error); // β TypeScript knows this is null
|
|
118
|
-
} else {
|
|
119
|
-
// TypeScript knows this is Err<E>
|
|
120
|
-
console.log(result.error); // β
error is type E
|
|
121
|
-
// console.log(result.data); // β TypeScript knows this is null
|
|
122
|
-
}
|
|
123
|
-
}
|
|
52
|
+
// TypeScript prevents mixing them up!
|
|
53
|
+
function getUser(id: UserId) { /* ... */ }
|
|
124
54
|
```
|
|
125
55
|
|
|
126
|
-
|
|
56
|
+
### π Tagged Errors
|
|
57
|
+
Structured, serializable errors with convenient factory functions
|
|
58
|
+
```typescript
|
|
59
|
+
import { createTaggedError } from "wellcrafted/error";
|
|
127
60
|
|
|
128
|
-
|
|
61
|
+
const { ApiError, ApiErr } = createTaggedError("ApiError");
|
|
62
|
+
// ApiError() creates error object, ApiErr() creates Err-wrapped error
|
|
63
|
+
```
|
|
129
64
|
|
|
130
65
|
## Installation
|
|
131
66
|
|
|
@@ -133,731 +68,423 @@ The beauty is in the transparency - you can see exactly how it works under the h
|
|
|
133
68
|
npm install wellcrafted
|
|
134
69
|
```
|
|
135
70
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
## Handling Operation Outcomes
|
|
139
|
-
|
|
140
|
-
Once you have a `Result`, there are two main patterns for working with it. Choose the pattern that best fits your preference for code style and the specific context of your code.
|
|
141
|
-
|
|
142
|
-
### Pattern 1: Destructuring (Preferred)
|
|
143
|
-
|
|
144
|
-
This pattern will feel familiar to developers working with modern libraries like Supabase or Astro Actions. You can destructure the `data` and `error` properties directly from the result object and use a simple conditional check on the `error` property.
|
|
145
|
-
|
|
146
|
-
This approach is often cleaner and more direct for handling the two possible outcomes, as it gives you immediate access to the inner `data` and `error` values.
|
|
71
|
+
## Quick Start
|
|
147
72
|
|
|
148
|
-
```
|
|
149
|
-
|
|
73
|
+
```typescript
|
|
74
|
+
import { tryAsync } from "wellcrafted/result";
|
|
75
|
+
import { createTaggedError } from "wellcrafted/error";
|
|
76
|
+
|
|
77
|
+
// Define your error with factory function
|
|
78
|
+
const { ApiError, ApiErr } = createTaggedError("ApiError");
|
|
79
|
+
type ApiError = ReturnType<typeof ApiError>;
|
|
80
|
+
|
|
81
|
+
// Wrap any throwing operation
|
|
82
|
+
const { data, error } = await tryAsync({
|
|
83
|
+
try: () => fetch('/api/user').then(r => r.json()),
|
|
84
|
+
mapError: (error) => ApiError({
|
|
85
|
+
message: "Failed to fetch user",
|
|
86
|
+
context: { endpoint: '/api/user' },
|
|
87
|
+
cause: error
|
|
88
|
+
})
|
|
89
|
+
});
|
|
150
90
|
|
|
151
91
|
if (error) {
|
|
152
|
-
|
|
153
|
-
console.error(`An error occurred: ${error.message}`);
|
|
154
|
-
return; // Or handle the error appropriately
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// If `error` is null, `data` holds the inner success value from the Ok variant.
|
|
158
|
-
// In most modern TypeScript setups, `data` will be correctly inferred as the success type.
|
|
159
|
-
console.log(`The result is: ${data}`);
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Pattern 2: Using Type Guards
|
|
163
|
-
|
|
164
|
-
In some complex scenarios or with certain TypeScript configurations, the compiler might not be able to perfectly infer the relationship between `data` and `error` when they are destructured into separate variables. In these cases, using the `isOk()` and `isErr()` type guards is a more robust solution. TypeScript's control flow analysis is designed to work flawlessly with this pattern, guaranteeing type safety within each conditional block.
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
import { isOk, isErr } from "wellcrafted/result";
|
|
168
|
-
|
|
169
|
-
const result = someOperation();
|
|
170
|
-
|
|
171
|
-
if (isErr(result)) {
|
|
172
|
-
// TypeScript *guarantees* that `result` is `Err<E>` here.
|
|
173
|
-
// The `result.data` property is `null`.
|
|
174
|
-
// The `result.error` property contains the error value.
|
|
175
|
-
const errorValue = result.error;
|
|
176
|
-
console.error(errorValue);
|
|
177
|
-
|
|
92
|
+
console.error(`${error.name}: ${error.message}`);
|
|
178
93
|
} else {
|
|
179
|
-
|
|
180
|
-
// TypeScript *guarantees* that `result` is `Ok<T>` here.
|
|
181
|
-
// The `result.error` property is `null`.
|
|
182
|
-
// The `result.data` property contains the success value.
|
|
183
|
-
const successValue = result.data;
|
|
184
|
-
console.log(successValue);
|
|
94
|
+
console.log("User:", data);
|
|
185
95
|
}
|
|
186
96
|
```
|
|
187
97
|
|
|
188
|
-
|
|
98
|
+
## Core Features
|
|
189
99
|
|
|
190
|
-
|
|
100
|
+
<table>
|
|
101
|
+
<tr>
|
|
102
|
+
<td>
|
|
191
103
|
|
|
192
|
-
|
|
104
|
+
**π― Explicit Error Handling**
|
|
105
|
+
All errors visible in function signatures
|
|
193
106
|
|
|
194
|
-
|
|
107
|
+
</td>
|
|
108
|
+
<td>
|
|
195
109
|
|
|
196
|
-
|
|
110
|
+
**π¦ Serialization-Safe**
|
|
111
|
+
Plain objects work everywhere
|
|
197
112
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
3. **Lightweight**: Avoids the overhead of class instantiation and the complexities of `instanceof` checks.
|
|
201
|
-
4. **Structured Context**: Easily enforce that all errors carry structured, machine-readable context.
|
|
113
|
+
</td>
|
|
114
|
+
<td>
|
|
202
115
|
|
|
203
|
-
|
|
116
|
+
**β¨ Elegant API**
|
|
117
|
+
Clean, intuitive patterns
|
|
204
118
|
|
|
205
|
-
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
<tr>
|
|
122
|
+
<td>
|
|
206
123
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
readonly name: T; // 1. The discriminant
|
|
210
|
-
message: string; // 2. Human-readable description
|
|
211
|
-
context: Record<string, unknown>; // 3. Debugging data
|
|
212
|
-
cause?: unknown; // 4. Root cause (optional)
|
|
213
|
-
};
|
|
214
|
-
```
|
|
124
|
+
**π Zero Magic**
|
|
125
|
+
~50 lines of core code
|
|
215
126
|
|
|
216
|
-
|
|
127
|
+
</td>
|
|
128
|
+
<td>
|
|
217
129
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
type ValidationError = TaggedError<"ValidationError">;
|
|
222
|
-
type NetworkError = TaggedError<"NetworkError">;
|
|
223
|
-
type FileError = TaggedError<"FileError">;
|
|
224
|
-
|
|
225
|
-
function handleError(error: ValidationError | NetworkError | FileError) {
|
|
226
|
-
switch (error.name) {
|
|
227
|
-
case "ValidationError":
|
|
228
|
-
// TypeScript knows this is ValidationError
|
|
229
|
-
console.log("Invalid input:", error.context);
|
|
230
|
-
break;
|
|
231
|
-
case "NetworkError":
|
|
232
|
-
// TypeScript knows this is NetworkError
|
|
233
|
-
console.log("Network failed:", error.message);
|
|
234
|
-
break;
|
|
235
|
-
case "FileError":
|
|
236
|
-
// TypeScript knows this is FileError
|
|
237
|
-
console.log("File issue:", error.context);
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
```
|
|
130
|
+
**π Lightweight**
|
|
131
|
+
Zero dependencies, < 2KB
|
|
242
132
|
|
|
243
|
-
|
|
133
|
+
</td>
|
|
134
|
+
<td>
|
|
244
135
|
|
|
245
|
-
|
|
136
|
+
**π¨ Composable**
|
|
137
|
+
Mix and match utilities
|
|
246
138
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
message: "Email address must contain an @ symbol", // Clear, specific
|
|
251
|
-
context: { email: userInput },
|
|
252
|
-
cause: undefined
|
|
253
|
-
});
|
|
254
|
-
```
|
|
139
|
+
</td>
|
|
140
|
+
</tr>
|
|
141
|
+
</table>
|
|
255
142
|
|
|
256
|
-
|
|
143
|
+
## The Result Pattern Explained
|
|
257
144
|
|
|
258
|
-
|
|
145
|
+
The Result type makes error handling explicit and type-safe:
|
|
259
146
|
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
context: {
|
|
266
|
-
userId: id, // Function input
|
|
267
|
-
options, // Function input
|
|
268
|
-
timestamp: new Date().toISOString(), // Additional context
|
|
269
|
-
retryCount: 3 // Useful debugging info
|
|
270
|
-
},
|
|
271
|
-
cause: undefined
|
|
272
|
-
});
|
|
273
|
-
}
|
|
147
|
+
```typescript
|
|
148
|
+
// The entire implementation
|
|
149
|
+
type Ok<T> = { data: T; error: null };
|
|
150
|
+
type Err<E> = { error: E; data: null };
|
|
151
|
+
type Result<T, E> = Ok<T> | Err<E>;
|
|
274
152
|
```
|
|
275
153
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
- **For new errors**: Set `cause: undefined`
|
|
279
|
-
- **For wrapping existing errors**: Pass the original error as `cause`
|
|
154
|
+
**The Magic**: This creates a discriminated union where TypeScript automatically narrows types:
|
|
280
155
|
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
context: { input },
|
|
287
|
-
cause: undefined // New error, no underlying cause
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// Wrapping an existing error
|
|
291
|
-
try {
|
|
292
|
-
await database.save(user);
|
|
293
|
-
} catch (dbError) {
|
|
294
|
-
return Err({
|
|
295
|
-
name: "SaveError",
|
|
296
|
-
message: "Failed to save user",
|
|
297
|
-
context: { userId: user.id },
|
|
298
|
-
cause: dbError // Bubble up the original database error
|
|
299
|
-
});
|
|
156
|
+
```typescript
|
|
157
|
+
if (result.error) {
|
|
158
|
+
// TypeScript knows: error is E, data is null
|
|
159
|
+
} else {
|
|
160
|
+
// TypeScript knows: data is T, error is null
|
|
300
161
|
}
|
|
301
162
|
```
|
|
302
163
|
|
|
303
|
-
|
|
164
|
+
## Basic Patterns
|
|
304
165
|
|
|
305
|
-
|
|
166
|
+
### Handle Results with Destructuring
|
|
306
167
|
|
|
307
168
|
```typescript
|
|
308
|
-
|
|
309
|
-
export type FileNotFoundError = TaggedError<"FileNotFoundError">;
|
|
310
|
-
export type PermissionDeniedError = TaggedError<"PermissionDeniedError">;
|
|
311
|
-
export type DiskFullError = TaggedError<"DiskFullError">;
|
|
312
|
-
|
|
313
|
-
// Create a union of all possible errors for this domain
|
|
314
|
-
export type FileSystemError = FileNotFoundError | PermissionDeniedError | DiskFullError;
|
|
169
|
+
const { data, error } = await someOperation();
|
|
315
170
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return
|
|
319
|
-
name: "FileNotFoundError",
|
|
320
|
-
message: `The file at path "${path}" was not found.`,
|
|
321
|
-
context: { path },
|
|
322
|
-
cause
|
|
323
|
-
};
|
|
171
|
+
if (error) {
|
|
172
|
+
// Handle error with full type safety
|
|
173
|
+
return;
|
|
324
174
|
}
|
|
325
|
-
```
|
|
326
175
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
```ts
|
|
330
|
-
function handleError(error: FileSystemError) {
|
|
331
|
-
switch (error.name) {
|
|
332
|
-
case "FileNotFoundError":
|
|
333
|
-
// TypeScript knows `error` is `FileNotFoundError` here.
|
|
334
|
-
console.error(`Path not found: ${error.context.path}`);
|
|
335
|
-
break;
|
|
336
|
-
case "PermissionDeniedError":
|
|
337
|
-
// TypeScript knows `error` is `PermissionDeniedError` here.
|
|
338
|
-
console.error("Permission was denied.");
|
|
339
|
-
break;
|
|
340
|
-
case "DiskFullError":
|
|
341
|
-
// ...
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
176
|
+
// Use data - TypeScript knows it's safe
|
|
345
177
|
```
|
|
346
178
|
|
|
347
|
-
###
|
|
348
|
-
|
|
349
|
-
#### 1. Include Meaningful Context
|
|
350
|
-
Always include function inputs and other relevant state in the `context` object. This is invaluable for logging and debugging.
|
|
179
|
+
### Wrap Unsafe Operations
|
|
351
180
|
|
|
352
181
|
```typescript
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
query,
|
|
364
|
-
params,
|
|
365
|
-
timestamp: new Date().toISOString(),
|
|
366
|
-
},
|
|
367
|
-
cause,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
#### 2. Handle Errors at the Right Level
|
|
373
|
-
Handle or transform errors where you can add more context or make a recovery decision.
|
|
374
|
-
|
|
375
|
-
```ts
|
|
376
|
-
async function initializeApp(): Promise<Result<App, FsError | ValidationError>> {
|
|
377
|
-
const configResult = await readConfig("./config.json");
|
|
378
|
-
|
|
379
|
-
// Propagate the file system error directly if config read fails
|
|
380
|
-
if (isErr(configResult)) {
|
|
381
|
-
return configResult;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// If config is read, but is invalid, return a *different* kind of error
|
|
385
|
-
const validationResult = validateConfig(configResult.data);
|
|
386
|
-
if (isErr(validationResult)) {
|
|
387
|
-
return validationResult;
|
|
388
|
-
}
|
|
182
|
+
// Synchronous
|
|
183
|
+
const result = trySync({
|
|
184
|
+
try: () => JSON.parse(jsonString),
|
|
185
|
+
mapError: (error) => ({
|
|
186
|
+
name: "ParseError",
|
|
187
|
+
message: "Invalid JSON",
|
|
188
|
+
context: { input: jsonString },
|
|
189
|
+
cause: error
|
|
190
|
+
})
|
|
191
|
+
});
|
|
389
192
|
|
|
390
|
-
|
|
391
|
-
|
|
193
|
+
// Asynchronous
|
|
194
|
+
const result = await tryAsync({
|
|
195
|
+
try: () => fetch(url),
|
|
196
|
+
mapError: (error) => ({
|
|
197
|
+
name: "NetworkError",
|
|
198
|
+
message: "Request failed",
|
|
199
|
+
context: { url },
|
|
200
|
+
cause: error
|
|
201
|
+
})
|
|
202
|
+
});
|
|
392
203
|
```
|
|
393
204
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
---
|
|
397
|
-
|
|
398
|
-
## Basic Usage
|
|
205
|
+
### Service Layer Example
|
|
399
206
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
import { Result, Ok, Err, isOk } from "wellcrafted/result";
|
|
404
|
-
import { type TaggedError } from "wellcrafted/error";
|
|
405
|
-
|
|
406
|
-
// --- Example 1: A Safe Division Function ---
|
|
207
|
+
```typescript
|
|
208
|
+
import { Result, Ok, tryAsync } from "wellcrafted/result";
|
|
209
|
+
import { createTaggedError } from "wellcrafted/error";
|
|
407
210
|
|
|
408
|
-
//
|
|
409
|
-
|
|
211
|
+
// Define service-specific errors
|
|
212
|
+
const { ValidationError, ValidationErr } = createTaggedError("ValidationError");
|
|
213
|
+
const { DatabaseError, DatabaseErr } = createTaggedError("DatabaseError");
|
|
410
214
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (denominator === 0) {
|
|
414
|
-
return Err({
|
|
415
|
-
name: "MathError",
|
|
416
|
-
message: "Cannot divide by zero.",
|
|
417
|
-
context: { numerator, denominator },
|
|
418
|
-
cause: undefined
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
return Ok(numerator / denominator);
|
|
422
|
-
}
|
|
215
|
+
type ValidationError = ReturnType<typeof ValidationError>;
|
|
216
|
+
type DatabaseError = ReturnType<typeof DatabaseError>;
|
|
423
217
|
|
|
424
|
-
//
|
|
425
|
-
|
|
218
|
+
// Factory function pattern - no classes!
|
|
219
|
+
export function createUserService(db: Database) {
|
|
220
|
+
return {
|
|
221
|
+
async createUser(input: CreateUserInput): Promise<Result<User, ValidationError | DatabaseError>> {
|
|
222
|
+
// Direct return with Err variant
|
|
223
|
+
if (!input.email.includes('@')) {
|
|
224
|
+
return ValidationErr({
|
|
225
|
+
message: "Invalid email format",
|
|
226
|
+
context: { field: 'email', value: input.email },
|
|
227
|
+
cause: undefined
|
|
228
|
+
});
|
|
229
|
+
}
|
|
426
230
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
231
|
+
return tryAsync({
|
|
232
|
+
try: () => db.save(input),
|
|
233
|
+
mapError: (error) => DatabaseError({
|
|
234
|
+
message: "Failed to save user",
|
|
235
|
+
context: { operation: 'createUser', input },
|
|
236
|
+
cause: error
|
|
237
|
+
})
|
|
238
|
+
});
|
|
239
|
+
},
|
|
432
240
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const data = JSON.parse(json);
|
|
442
|
-
if (typeof data.name !== "string") {
|
|
443
|
-
return Err({
|
|
444
|
-
name: "ParseError",
|
|
445
|
-
message: "User object must have a name property of type string.",
|
|
446
|
-
context: { receivedValue: data.name },
|
|
447
|
-
cause: undefined
|
|
241
|
+
async getUser(id: string): Promise<Result<User | null, DatabaseError>> {
|
|
242
|
+
return tryAsync({
|
|
243
|
+
try: () => db.findById(id),
|
|
244
|
+
mapError: (error) => DatabaseError({
|
|
245
|
+
message: "Failed to fetch user",
|
|
246
|
+
context: { userId: id },
|
|
247
|
+
cause: error
|
|
248
|
+
})
|
|
448
249
|
});
|
|
449
250
|
}
|
|
450
|
-
|
|
451
|
-
} catch (e) {
|
|
452
|
-
return Err({
|
|
453
|
-
name: "ParseError",
|
|
454
|
-
message: "Invalid JSON provided.",
|
|
455
|
-
context: { rawString: json },
|
|
456
|
-
cause: e,
|
|
457
|
-
});
|
|
458
|
-
}
|
|
251
|
+
};
|
|
459
252
|
}
|
|
460
253
|
|
|
461
|
-
//
|
|
462
|
-
|
|
254
|
+
// Export type for the service
|
|
255
|
+
export type UserService = ReturnType<typeof createUserService>;
|
|
463
256
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
} else {
|
|
467
|
-
// `userResult.error` is a fully-typed ParseError object
|
|
468
|
-
console.error(`Error (${userResult.error.name}): ${userResult.error.message}`);
|
|
469
|
-
console.log("Context:", userResult.error.context);
|
|
470
|
-
}
|
|
257
|
+
// Create a live instance (dependency injection at build time)
|
|
258
|
+
export const UserServiceLive = createUserService(databaseInstance);
|
|
471
259
|
```
|
|
472
260
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
## Wrapping Functions That Throw
|
|
261
|
+
## Why wellcrafted?
|
|
476
262
|
|
|
477
|
-
|
|
263
|
+
JavaScript's `try-catch` has fundamental problems:
|
|
478
264
|
|
|
479
|
-
|
|
265
|
+
1. **Invisible Errors**: Function signatures don't show what errors can occur
|
|
266
|
+
2. **Lost in Transit**: `JSON.stringify(new Error())` loses critical information
|
|
267
|
+
3. **No Type Safety**: TypeScript can't help with `catch (error)` blocks
|
|
268
|
+
4. **Inconsistent**: Libraries throw different things (strings, errors, objects, undefined)
|
|
480
269
|
|
|
481
|
-
|
|
270
|
+
wellcrafted solves these with simple, composable primitives that make errors:
|
|
271
|
+
- **Explicit** in function signatures
|
|
272
|
+
- **Serializable** across all boundaries
|
|
273
|
+
- **Type-safe** with full TypeScript support
|
|
274
|
+
- **Consistent** with structured error objects
|
|
482
275
|
|
|
483
|
-
|
|
484
|
-
import { trySync, Result } from "wellcrafted/result";
|
|
485
|
-
import { type TaggedError } from "wellcrafted/error";
|
|
276
|
+
## Service Pattern Best Practices
|
|
486
277
|
|
|
487
|
-
|
|
278
|
+
Based on real-world usage, here's the recommended pattern for creating services with wellcrafted:
|
|
488
279
|
|
|
489
|
-
|
|
490
|
-
return trySync({
|
|
491
|
-
try: () => JSON.parse(raw),
|
|
492
|
-
mapError: (error) => ({
|
|
493
|
-
name: "ParseError",
|
|
494
|
-
message: "Failed to parse JSON",
|
|
495
|
-
context: { raw },
|
|
496
|
-
cause: error
|
|
497
|
-
})
|
|
498
|
-
});
|
|
499
|
-
}
|
|
280
|
+
### Factory Function Pattern
|
|
500
281
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
throw { message: "Request failed", statusCode: response.status };
|
|
282
|
+
```typescript
|
|
283
|
+
import { createTaggedError } from "wellcrafted/error";
|
|
284
|
+
|
|
285
|
+
// 1. Define service-specific errors
|
|
286
|
+
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError");
|
|
287
|
+
type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
|
|
288
|
+
|
|
289
|
+
// 2. Create service with factory function
|
|
290
|
+
export function createRecorderService() {
|
|
291
|
+
// Private state in closure
|
|
292
|
+
let isRecording = false;
|
|
293
|
+
|
|
294
|
+
// Return object with methods
|
|
295
|
+
return {
|
|
296
|
+
startRecording(): Result<void, RecorderServiceError> {
|
|
297
|
+
if (isRecording) {
|
|
298
|
+
return RecorderServiceErr({
|
|
299
|
+
message: "Already recording",
|
|
300
|
+
context: { isRecording },
|
|
301
|
+
cause: undefined
|
|
302
|
+
});
|
|
523
303
|
}
|
|
524
|
-
|
|
304
|
+
|
|
305
|
+
isRecording = true;
|
|
306
|
+
return Ok(undefined);
|
|
525
307
|
},
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
308
|
+
|
|
309
|
+
stopRecording(): Result<Blob, RecorderServiceError> {
|
|
310
|
+
if (!isRecording) {
|
|
311
|
+
return RecorderServiceErr({
|
|
312
|
+
message: "Not currently recording",
|
|
313
|
+
context: { isRecording },
|
|
314
|
+
cause: undefined
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
isRecording = false;
|
|
319
|
+
return Ok(new Blob(["audio data"]));
|
|
320
|
+
}
|
|
321
|
+
};
|
|
533
322
|
}
|
|
534
323
|
|
|
535
|
-
|
|
536
|
-
|
|
324
|
+
// 3. Export type
|
|
325
|
+
export type RecorderService = ReturnType<typeof createRecorderService>;
|
|
537
326
|
|
|
538
|
-
|
|
327
|
+
// 4. Create singleton instance
|
|
328
|
+
export const RecorderServiceLive = createRecorderService();
|
|
329
|
+
```
|
|
539
330
|
|
|
540
|
-
|
|
331
|
+
### Platform-Specific Services
|
|
541
332
|
|
|
542
|
-
|
|
333
|
+
For services that need different implementations per platform:
|
|
543
334
|
|
|
544
|
-
|
|
335
|
+
```typescript
|
|
336
|
+
// types.ts - shared interface
|
|
337
|
+
export type FileService = {
|
|
338
|
+
readFile(path: string): Promise<Result<string, FileServiceError>>;
|
|
339
|
+
writeFile(path: string, content: string): Promise<Result<void, FileServiceError>>;
|
|
340
|
+
};
|
|
545
341
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const content = await fs.readFile(path, 'utf-8');
|
|
552
|
-
return content;
|
|
342
|
+
// desktop.ts
|
|
343
|
+
export function createFileServiceDesktop(): FileService {
|
|
344
|
+
return {
|
|
345
|
+
async readFile(path) {
|
|
346
|
+
// Desktop implementation using Node.js APIs
|
|
553
347
|
},
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
cause: error
|
|
559
|
-
})
|
|
560
|
-
});
|
|
348
|
+
async writeFile(path, content) {
|
|
349
|
+
// Desktop implementation
|
|
350
|
+
}
|
|
351
|
+
};
|
|
561
352
|
}
|
|
562
353
|
|
|
563
|
-
//
|
|
564
|
-
function
|
|
565
|
-
return
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
validateConfig(parsed); // throws if invalid
|
|
569
|
-
return parsed as Config;
|
|
354
|
+
// web.ts
|
|
355
|
+
export function createFileServiceWeb(): FileService {
|
|
356
|
+
return {
|
|
357
|
+
async readFile(path) {
|
|
358
|
+
// Web implementation using File API
|
|
570
359
|
},
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
cause: error
|
|
576
|
-
})
|
|
577
|
-
});
|
|
360
|
+
async writeFile(path, content) {
|
|
361
|
+
// Web implementation
|
|
362
|
+
}
|
|
363
|
+
};
|
|
578
364
|
}
|
|
579
|
-
```
|
|
580
365
|
|
|
581
|
-
|
|
366
|
+
// index.ts - runtime selection
|
|
367
|
+
export const FileServiceLive = typeof window !== 'undefined'
|
|
368
|
+
? createFileServiceWeb()
|
|
369
|
+
: createFileServiceDesktop();
|
|
370
|
+
```
|
|
582
371
|
|
|
583
|
-
|
|
372
|
+
## Common Use Cases
|
|
584
373
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
async function saveUser(user: User) {
|
|
588
|
-
return tryAsync({
|
|
589
|
-
try: async () => {
|
|
590
|
-
const result = await db.users.insert(user);
|
|
591
|
-
return result.id;
|
|
592
|
-
},
|
|
593
|
-
mapError: (error): DatabaseError => ({ // π Annotate return type
|
|
594
|
-
name: "DatabaseError",
|
|
595
|
-
message: "Failed to save user to database",
|
|
596
|
-
context: { userId: user.id },
|
|
597
|
-
cause: error
|
|
598
|
-
})
|
|
599
|
-
});
|
|
600
|
-
}
|
|
374
|
+
<details>
|
|
375
|
+
<summary><b>API Route Handler</b></summary>
|
|
601
376
|
|
|
602
|
-
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
})
|
|
617
|
-
});
|
|
377
|
+
```typescript
|
|
378
|
+
export async function GET(request: Request) {
|
|
379
|
+
const result = await userService.getUser(params.id);
|
|
380
|
+
|
|
381
|
+
if (result.error) {
|
|
382
|
+
switch (result.error.name) {
|
|
383
|
+
case "UserNotFoundError":
|
|
384
|
+
return new Response("Not found", { status: 404 });
|
|
385
|
+
case "DatabaseError":
|
|
386
|
+
return new Response("Server error", { status: 500 });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return Response.json(result.data);
|
|
618
391
|
}
|
|
619
392
|
```
|
|
393
|
+
</details>
|
|
620
394
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
- Avoid using `as const` - always map to proper TaggedError objects
|
|
624
|
-
- Choose explicit generics for clarity, or return type annotation for brevity
|
|
625
|
-
- The important thing is ensuring type safety for your error handling
|
|
626
|
-
|
|
627
|
-
---
|
|
628
|
-
|
|
629
|
-
## Partitioning Results
|
|
630
|
-
|
|
631
|
-
When working with multiple asynchronous operations that return `Result` objects, you'll often need to separate the successful results from the failed ones. The `partitionResults` utility function makes this easy by splitting an array of Results into two separate arrays.
|
|
632
|
-
|
|
633
|
-
### When to Use `partitionResults`
|
|
395
|
+
<details>
|
|
396
|
+
<summary><b>Form Validation</b></summary>
|
|
634
397
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
return executeCommand({ command, config });
|
|
654
|
-
})
|
|
655
|
-
.filter((result) => result !== undefined) // Remove undefined values
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
const { oks, errs } = partitionResults(results);
|
|
659
|
-
|
|
660
|
-
// Handle all errors at once
|
|
661
|
-
if (errs.length > 0) {
|
|
662
|
-
const errorMessages = errs.map(({ error }) => error.message).join(', ');
|
|
663
|
-
showNotification(`${errs.length} operations failed: ${errorMessages}`);
|
|
664
|
-
return;
|
|
398
|
+
```typescript
|
|
399
|
+
function validateLoginForm(data: unknown): Result<LoginData, FormError> {
|
|
400
|
+
const errors: Record<string, string[]> = {};
|
|
401
|
+
|
|
402
|
+
if (!isValidEmail(data?.email)) {
|
|
403
|
+
errors.email = ["Invalid email format"];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (Object.keys(errors).length > 0) {
|
|
407
|
+
return Err({
|
|
408
|
+
name: "FormError",
|
|
409
|
+
message: "Validation failed",
|
|
410
|
+
context: { fields: errors },
|
|
411
|
+
cause: undefined
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return Ok(data as LoginData);
|
|
665
416
|
}
|
|
666
|
-
|
|
667
|
-
return oks.map(ok => ok.data); // Return processed content
|
|
668
417
|
```
|
|
418
|
+
</details>
|
|
669
419
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
```ts
|
|
673
|
-
import { tryAsync, partitionResults } from "wellcrafted/result";
|
|
674
|
-
import { type TaggedError } from "wellcrafted/error";
|
|
675
|
-
import * as fs from 'fs/promises';
|
|
676
|
-
|
|
677
|
-
type FileError = TaggedError<"FileError">;
|
|
678
|
-
|
|
679
|
-
async function processFiles(filePaths: string[]) {
|
|
680
|
-
// Map each file path to a Result-returning operation
|
|
681
|
-
const results = await Promise.all(
|
|
682
|
-
filePaths
|
|
683
|
-
.map((path) => {
|
|
684
|
-
if (!path.endsWith('.txt')) return; // Skip non-text files
|
|
685
|
-
return tryAsync<string, FileError>({
|
|
686
|
-
try: async () => {
|
|
687
|
-
const content = await fs.readFile(path, 'utf-8');
|
|
688
|
-
return content.toUpperCase(); // Process the content
|
|
689
|
-
},
|
|
690
|
-
mapError: (error) => ({
|
|
691
|
-
name: "FileError",
|
|
692
|
-
message: "Failed to process file",
|
|
693
|
-
context: { path },
|
|
694
|
-
cause: error
|
|
695
|
-
})
|
|
696
|
-
});
|
|
697
|
-
})
|
|
698
|
-
.filter((result) => result !== undefined)
|
|
699
|
-
);
|
|
700
|
-
|
|
701
|
-
const { oks, errs } = partitionResults(results);
|
|
420
|
+
<details>
|
|
421
|
+
<summary><b>React Hook</b></summary>
|
|
702
422
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
423
|
+
```typescript
|
|
424
|
+
function useUser(id: number) {
|
|
425
|
+
const [state, setState] = useState<{
|
|
426
|
+
loading: boolean;
|
|
427
|
+
user?: User;
|
|
428
|
+
error?: ApiError;
|
|
429
|
+
}>({ loading: true });
|
|
430
|
+
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
fetchUser(id).then(result => {
|
|
433
|
+
if (result.error) {
|
|
434
|
+
setState({ loading: false, error: result.error });
|
|
435
|
+
} else {
|
|
436
|
+
setState({ loading: false, user: result.data });
|
|
437
|
+
}
|
|
708
438
|
});
|
|
709
|
-
}
|
|
439
|
+
}, [id]);
|
|
710
440
|
|
|
711
|
-
|
|
712
|
-
if (oks.length > 0) {
|
|
713
|
-
console.log(`Successfully processed ${oks.length} files`);
|
|
714
|
-
return oks.map(ok => ok.data); // Return processed content
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return [];
|
|
441
|
+
return state;
|
|
718
442
|
}
|
|
719
443
|
```
|
|
444
|
+
</details>
|
|
720
445
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
1. **Batch Error Handling**: Instead of stopping at the first error, you can collect all failures and present them together
|
|
724
|
-
2. **Type Safety**: The returned `oks` and `errs` arrays are properly typed as `Ok<T>[]` and `Err<E>[]` respectively
|
|
725
|
-
3. **Clean Separation**: Successful and failed operations are cleanly separated for different handling logic
|
|
726
|
-
4. **Composability**: Works seamlessly with the map β filter β partition pattern for complex data processing
|
|
446
|
+
## Comparison with Alternatives
|
|
727
447
|
|
|
728
|
-
|
|
448
|
+
| | wellcrafted | fp-ts | Effect | neverthrow |
|
|
449
|
+
|---|---|---|---|---|
|
|
450
|
+
| **Learning Curve** | Minimal | Steep | Steep | Moderate |
|
|
451
|
+
| **Syntax** | Native async/await | Pipe operators | Generators | Method chains |
|
|
452
|
+
| **Bundle Size** | < 2KB | ~30KB | ~50KB | ~5KB |
|
|
453
|
+
| **Type Safety** | β
Full | β
Full | β
Full | β
Full |
|
|
454
|
+
| **Serializable Errors** | β
Built-in | β Classes | β Classes | β Classes |
|
|
729
455
|
|
|
730
456
|
## API Reference
|
|
731
457
|
|
|
732
|
-
###
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
| `trySync()` | Wrap throwing function | `trySync({ try: () => JSON.parse(str) })` |
|
|
741
|
-
| `tryAsync()` | Wrap async throwing function | `tryAsync({ try: () => fetch(url) })` |
|
|
742
|
-
| `partitionResults()` | Split Results into oks/errs | `const { oks, errs } = partitionResults(results)` |
|
|
743
|
-
|
|
744
|
-
### Detailed API
|
|
745
|
-
|
|
746
|
-
#### Types
|
|
747
|
-
- **`Result<T, E>`**: The core union type, representing `Ok<T> | Err<E>`.
|
|
748
|
-
- **`Ok<T>`**: Represents a success. Contains `{ data: T; error: null; }`.
|
|
749
|
-
- **`Err<E>`**: Represents a failure. Contains `{ data: null; error: E; }`.
|
|
750
|
-
- **`BaseError` / `TaggedError<T>`**: Helpers for creating a structured error system.
|
|
751
|
-
|
|
752
|
-
#### Core Result Functions
|
|
753
|
-
- **`Ok(data)`**: Creates a success `Result`.
|
|
754
|
-
- **`Err(error)`**: Creates a failure `Result`.
|
|
755
|
-
- **`isOk(result)`**: Type guard. Returns `true` if the result is an `Ok` variant.
|
|
756
|
-
- **`isErr(result)`**: Type guard. Returns `true` if the result is an `Err` variant.
|
|
757
|
-
- **`unwrap(result)`**: Unwraps a `Result`, returning data on `Ok` or throwing error on `Err`.
|
|
758
|
-
- **`resolve(value)`**: Resolves a value that may or may not be a `Result`, returning the final value or throwing on `Err`.
|
|
759
|
-
- **`isResult(value)`**: Type guard. Returns `true` if a value has the shape of a `Result`.
|
|
760
|
-
|
|
761
|
-
#### Async/Sync Wrappers
|
|
762
|
-
- **`trySync({ try, mapError })`**: Wraps a synchronous function that may throw.
|
|
763
|
-
- **`tryAsync({ try, mapError })`**: Wraps an asynchronous function that may throw or reject.
|
|
764
|
-
|
|
765
|
-
#### Error Utilities
|
|
766
|
-
- **`extractErrorMessage(error)`**: Safely extracts a string message from any error value.
|
|
767
|
-
|
|
768
|
-
#### Utility Functions
|
|
769
|
-
- **`partitionResults(results)`**: Partitions an array of Results into separate arrays of `Ok` and `Err` variants.
|
|
770
|
-
|
|
771
|
-
---
|
|
772
|
-
|
|
773
|
-
## Design Philosophy
|
|
774
|
-
|
|
775
|
-
This library is built on a set of core principles designed to create a robust, predictable, and developer-friendly experience. Understanding these principles will help you get the most out of the library and see why its API is designed the way it is.
|
|
776
|
-
|
|
777
|
-
### 1. Embrace JavaScript Primitives
|
|
778
|
-
|
|
779
|
-
A fundamental disagreement we have with some otherwise excellent libraries is the idea that JavaScript's core abstractions need to be completely reinvented. While we have immense respect for the power and type-level ingenuity of ecosystems like Effect-TS, we believe the cost of onboarding developers to an entirely new programming paradigm (like generators for async control flow) is too high for most projects.
|
|
780
|
-
|
|
781
|
-
This library is built on the philosophy of leaning into JavaScript's native primitives whenever they are "good enough." We prefer to build on the familiar foundations of `async/await`, `Promise`, and standard union types (`T | null`) because they are already well-understood by the vast majority of TypeScript developers. This drastically reduces the learning curve and makes the library easy to adopt incrementally.
|
|
458
|
+
### Result Functions
|
|
459
|
+
- **`Ok(data)`** - Create success result
|
|
460
|
+
- **`Err(error)`** - Create failure result
|
|
461
|
+
- **`isOk(result)`** - Type guard for success
|
|
462
|
+
- **`isErr(result)`** - Type guard for failure
|
|
463
|
+
- **`trySync(options)`** - Wrap throwing function
|
|
464
|
+
- **`tryAsync(options)`** - Wrap async function
|
|
465
|
+
- **`partitionResults(results)`** - Split array into oks/errs
|
|
782
466
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
467
|
+
### Error Functions
|
|
468
|
+
- **`createTaggedError(name)`** - Creates error factory functions
|
|
469
|
+
- Returns two functions: `{ErrorName}` and `{ErrorName}Err`
|
|
470
|
+
- The first creates plain error objects
|
|
471
|
+
- The second creates Err-wrapped errors
|
|
786
472
|
|
|
787
|
-
|
|
473
|
+
### Types
|
|
474
|
+
- **`Result<T, E>`** - Union of Ok<T> | Err<E>
|
|
475
|
+
- **`TaggedError<T>`** - Structured error type
|
|
476
|
+
- **`Brand<T, B>`** - Branded type wrapper
|
|
788
477
|
|
|
789
|
-
|
|
478
|
+
## Learn More
|
|
790
479
|
|
|
791
|
-
|
|
480
|
+
- π [Full Documentation](https://github.com/your-repo/wellcrafted/wiki)
|
|
481
|
+
- π [Examples](https://github.com/your-repo/wellcrafted/tree/main/examples)
|
|
482
|
+
- π¬ [Discussions](https://github.com/your-repo/wellcrafted/discussions)
|
|
792
483
|
|
|
793
|
-
|
|
484
|
+
## License
|
|
794
485
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
Every function is exported as a pure, standalone module, making the entire library **tree-shakable**. If you only use the `Result` type and the `isOk` function, the rest of the library's code won't be included in your application's build.
|
|
798
|
-
|
|
799
|
-
We believe a library should have a focused scope and not be overwhelming. While comprehensive ecosystems like Effect-TS are incredibly powerful, their scope can be daunting. This library aims to solve the specific and critical problem of type-safe error handling without pulling in a large, all-encompassing framework. It's a small tool that does one job well.
|
|
800
|
-
|
|
801
|
-
### 4. Serialization-First
|
|
802
|
-
|
|
803
|
-
A core requirement of this library is that all of its data structures, especially errors, must be reliably serializable. They need to behave identically whether you are passing them between functions, sending them over a network (HTTP), or passing them to a web worker. This is why the library fundamentally avoids classes for its error-handling system and instead promotes plain objects.
|
|
804
|
-
|
|
805
|
-
### 5. Opinionated yet Flexible
|
|
806
|
-
|
|
807
|
-
This library is opinionated in that it provides a clear, recommended path for best practices. We believe that a degree of standardization leads to more maintainable and predictable codebases. However, these opinions are not enforced at a technical level. The core `Result` type is deliberately decoupled from the error system, meaning you are free to use a different error implementation if your project requires it.
|
|
808
|
-
|
|
809
|
-
## Inspirations and Relationship to Effect-TS
|
|
810
|
-
|
|
811
|
-
This library's approach is heavily inspired by the powerful concepts pioneered by the **[Effect-TS](https://github.com/Effect-TS/effect)** ecosystem. Effect has indelibly shaped our thinking on how to structure services, handle errors, and compose applications in a type-safe way.
|
|
812
|
-
|
|
813
|
-
However, this library represents a different set of trade-offs and priorities, based on a few key disagreements with the Effect-TS approach:
|
|
814
|
-
|
|
815
|
-
1. **Familiarity Over Novelty**: While we agree that Promises can be a flawed abstraction, we believe the cost of replacing them entirely is too high for most teams. Effect introduces a new, powerful, but unfamiliar execution model based on generators (`yield`), which requires a significant investment to learn. This library chooses to embrace the familiar patterns of `async/await` and Promises, even with their imperfections, to ensure a gentle learning curve. The goal is to provide 80% of the benefit with 20% of the learning curve.
|
|
816
|
-
|
|
817
|
-
2. **Simplicity and Lightweight Integration**: We aim for this library to be as lightweight as possible, easy to adopt incrementally, and simple to integrate with other tools. It is not an all-encompassing application framework but rather a focused tool to solve the specific problem of `Result`-based error handling.
|
|
818
|
-
|
|
819
|
-
That said, the influence of Effect is clear. Functions like `trySync` and `tryAsync` are directly inspired by similar utilities in Effect. The core difference is that we aim to apply these powerful concepts on top of familiar JavaScript primitives, rather than creating a new ecosystem around them. This philosophy also informs our decision to omit an `Option<T>` type, as we believe that native TypeScript features (`T | null`, optional chaining, and nullish coalescing) are "good enough" and more idiomatic for the majority of use cases.
|
|
486
|
+
MIT
|
|
820
487
|
|
|
821
488
|
---
|
|
822
489
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
### Why `{ data, error }` instead of a boolean flag like `{ ok: boolean, ... }`?
|
|
826
|
-
|
|
827
|
-
Some libraries use a boolean flag for their discriminated union, like `{ ok: true, data: T } | { ok: false, error: E }`. While a valid pattern, we chose the `{ data, error }` shape for two main reasons:
|
|
828
|
-
|
|
829
|
-
1. **Ergonomics and Familiarity**: The destructuring pattern `const { data, error } = operation()` is clean and will feel familiar to developers using modern libraries like Supabase and Astro Actions. It provides immediate access to the inner values without an extra layer of property access. Checking a boolean flag first (`if (result.ok)`) and then accessing the value (`result.data`) is slightly more verbose.
|
|
830
|
-
|
|
831
|
-
2. **Lack of Standardization**: The boolean flag approach isn't standardized. Zod's `.safeParse`, for example, returns `{ success: boolean, ... }`. By adopting the `{ data, error }` pattern, we align with a simple, common, and intuitive structure for handling success and failure states in modern JavaScript.
|
|
832
|
-
|
|
833
|
-
**Note**: The `{ data, error }` pattern is also a discriminated unionβyou can use either `data` or `error` as the discriminant key and check if either of them is null. This creates the same type-narrowing benefits as a boolean flag while maintaining cleaner destructuring ergonomics.
|
|
834
|
-
|
|
835
|
-
### What's the difference between an `Err` variant and an `error` value?
|
|
836
|
-
|
|
837
|
-
This is a key distinction in the library's terminology:
|
|
838
|
-
|
|
839
|
-
- **`Err<E>` (The Variant/Container)**: This is one of the two possible "shapes" of a `Result` object. It's the wrapper itself, whose structure is `{ data: null, error: E }`. You can think of it as the box that signifies a failure.
|
|
840
|
-
|
|
841
|
-
- **`error` (The Value/Payload)**: This is the actual *value* inside the `Err` container. It is the content of the `error` property on the `Err` object. This is the piece of data that describes what went wrong, and its type is `E`.
|
|
842
|
-
|
|
843
|
-
When you use the `isErr()` type guard, you are checking if a `Result` is the `Err` variant. Once that check passes, you can then access the `.error` property to get the error value.
|
|
844
|
-
|
|
845
|
-
### Why doesn't this library include an `Option<T>` type?
|
|
846
|
-
|
|
847
|
-
An `Option<T>` type (sometimes called `Maybe`) is common in other languages to represent a value that might be missing. However, we've intentionally omitted it because **modern JavaScript and TypeScript already have excellent, first-class support for handling potentially missing values.**
|
|
848
|
-
|
|
849
|
-
A custom `Option<T>` type would add a layer of abstraction that is largely unnecessary. Instead, you can and should use:
|
|
850
|
-
|
|
851
|
-
1. **Union Types with `null`**: Simply type your value as `T | null`. This is the idiomatic way to represent an optional value in TypeScript.
|
|
852
|
-
|
|
853
|
-
2. **Optional Chaining (`?.`)**: Safely access nested properties of an object that might be null or undefined.
|
|
854
|
-
```ts
|
|
855
|
-
const street = user?.address?.street; // Returns undefined if user or address is null/undefined
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
3. **Nullish Coalescing (`??`)**: Provide a default value for a `null` or `undefined` expression.
|
|
859
|
-
```ts
|
|
860
|
-
const displayName = user.name ?? "Guest";
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
These built-in language features provide better ergonomics and are more familiar to JavaScript developers than a custom `Option` type would be. This library focuses on solving for `Result`, where the language does not have a built-in equivalent.
|
|
490
|
+
Made with β€οΈ by developers who believe error handling should be delightful.
|