wellcrafted 0.18.0 → 0.19.1
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 -729
- package/dist/error/index.d.ts +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 -28
- package/dist/result/index.js +2 -28
- package/dist/result-BCv06xhR.js +30 -0
- package/dist/result-BCv06xhR.js.map +1 -0
- package/dist/{result-Bi5hwqKw.d.ts → result-t0dngyiE.d.ts} +1 -1
- package/dist/{result-Bi5hwqKw.d.ts.map → result-t0dngyiE.d.ts.map} +1 -1
- 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,733 +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. Function inputs & debugging data (optional)
|
|
212
|
-
cause: unknown; // 4. Root cause
|
|
213
|
-
};
|
|
214
|
-
```
|
|
124
|
+
**🔍 Zero Magic**
|
|
125
|
+
~50 lines of core code
|
|
215
126
|
|
|
216
|
-
>
|
|
127
|
+
</td>
|
|
128
|
+
<td>
|
|
217
129
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
This is your error's unique identifier and the key to pattern matching. Use it in `if` statements and `switch` statements to handle different error types:
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
type ValidationError = TaggedError<"ValidationError">;
|
|
224
|
-
type NetworkError = TaggedError<"NetworkError">;
|
|
225
|
-
type FileError = TaggedError<"FileError">;
|
|
226
|
-
|
|
227
|
-
function handleError(error: ValidationError | NetworkError | FileError) {
|
|
228
|
-
switch (error.name) {
|
|
229
|
-
case "ValidationError":
|
|
230
|
-
// TypeScript knows this is ValidationError
|
|
231
|
-
console.log("Invalid input:", error.context);
|
|
232
|
-
break;
|
|
233
|
-
case "NetworkError":
|
|
234
|
-
// TypeScript knows this is NetworkError
|
|
235
|
-
console.log("Network failed:", error.message);
|
|
236
|
-
break;
|
|
237
|
-
case "FileError":
|
|
238
|
-
// TypeScript knows this is FileError
|
|
239
|
-
console.log("File issue:", error.context);
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
```
|
|
130
|
+
**🚀 Lightweight**
|
|
131
|
+
Zero dependencies, < 2KB
|
|
244
132
|
|
|
245
|
-
|
|
133
|
+
</td>
|
|
134
|
+
<td>
|
|
246
135
|
|
|
247
|
-
|
|
136
|
+
**🎨 Composable**
|
|
137
|
+
Mix and match utilities
|
|
248
138
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
message: "Email address must contain an @ symbol", // Clear, specific
|
|
253
|
-
context: { email: userInput },
|
|
254
|
-
cause: undefined
|
|
255
|
-
});
|
|
256
|
-
```
|
|
139
|
+
</td>
|
|
140
|
+
</tr>
|
|
141
|
+
</table>
|
|
257
142
|
|
|
258
|
-
|
|
143
|
+
## The Result Pattern Explained
|
|
259
144
|
|
|
260
|
-
The
|
|
145
|
+
The Result type makes error handling explicit and type-safe:
|
|
261
146
|
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
context: {
|
|
268
|
-
userId: id, // Function input
|
|
269
|
-
options, // Function input
|
|
270
|
-
timestamp: new Date().toISOString(), // Additional context
|
|
271
|
-
retryCount: 3 // Useful debugging info
|
|
272
|
-
},
|
|
273
|
-
cause: undefined
|
|
274
|
-
});
|
|
275
|
-
}
|
|
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>;
|
|
276
152
|
```
|
|
277
153
|
|
|
278
|
-
|
|
154
|
+
**The Magic**: This creates a discriminated union where TypeScript automatically narrows types:
|
|
279
155
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
return Err({
|
|
286
|
-
name: "ValidationError",
|
|
287
|
-
message: "Invalid user data",
|
|
288
|
-
context: { input },
|
|
289
|
-
cause: undefined // New error, no underlying cause
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Wrapping an existing error
|
|
293
|
-
try {
|
|
294
|
-
await database.save(user);
|
|
295
|
-
} catch (dbError) {
|
|
296
|
-
return Err({
|
|
297
|
-
name: "SaveError",
|
|
298
|
-
message: "Failed to save user",
|
|
299
|
-
context: { userId: user.id },
|
|
300
|
-
cause: dbError // Bubble up the original database error
|
|
301
|
-
});
|
|
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
|
|
302
161
|
}
|
|
303
162
|
```
|
|
304
163
|
|
|
305
|
-
|
|
164
|
+
## Basic Patterns
|
|
306
165
|
|
|
307
|
-
|
|
166
|
+
### Handle Results with Destructuring
|
|
308
167
|
|
|
309
168
|
```typescript
|
|
310
|
-
|
|
311
|
-
export type FileNotFoundError = TaggedError<"FileNotFoundError">;
|
|
312
|
-
export type PermissionDeniedError = TaggedError<"PermissionDeniedError">;
|
|
313
|
-
export type DiskFullError = TaggedError<"DiskFullError">;
|
|
314
|
-
|
|
315
|
-
// Create a union of all possible errors for this domain
|
|
316
|
-
export type FileSystemError = FileNotFoundError | PermissionDeniedError | DiskFullError;
|
|
169
|
+
const { data, error } = await someOperation();
|
|
317
170
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return
|
|
321
|
-
name: "FileNotFoundError",
|
|
322
|
-
message: `The file at path "${path}" was not found.`,
|
|
323
|
-
context: { path },
|
|
324
|
-
cause
|
|
325
|
-
};
|
|
171
|
+
if (error) {
|
|
172
|
+
// Handle error with full type safety
|
|
173
|
+
return;
|
|
326
174
|
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
Because `name` is a unique literal type for each error, TypeScript can use it to discriminate between them in a `switch` statement:
|
|
330
175
|
|
|
331
|
-
|
|
332
|
-
function handleError(error: FileSystemError) {
|
|
333
|
-
switch (error.name) {
|
|
334
|
-
case "FileNotFoundError":
|
|
335
|
-
// TypeScript knows `error` is `FileNotFoundError` here.
|
|
336
|
-
console.error(`Path not found: ${error.context.path}`);
|
|
337
|
-
break;
|
|
338
|
-
case "PermissionDeniedError":
|
|
339
|
-
// TypeScript knows `error` is `PermissionDeniedError` here.
|
|
340
|
-
console.error("Permission was denied.");
|
|
341
|
-
break;
|
|
342
|
-
case "DiskFullError":
|
|
343
|
-
// ...
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
176
|
+
// Use data - TypeScript knows it's safe
|
|
347
177
|
```
|
|
348
178
|
|
|
349
|
-
###
|
|
350
|
-
|
|
351
|
-
#### 1. Include Meaningful Context
|
|
352
|
-
Always include function inputs and other relevant state in the `context` object. This is invaluable for logging and debugging.
|
|
179
|
+
### Wrap Unsafe Operations
|
|
353
180
|
|
|
354
181
|
```typescript
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
query,
|
|
366
|
-
params,
|
|
367
|
-
timestamp: new Date().toISOString(),
|
|
368
|
-
},
|
|
369
|
-
cause,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
#### 2. Handle Errors at the Right Level
|
|
375
|
-
Handle or transform errors where you can add more context or make a recovery decision.
|
|
376
|
-
|
|
377
|
-
```ts
|
|
378
|
-
async function initializeApp(): Promise<Result<App, FsError | ValidationError>> {
|
|
379
|
-
const configResult = await readConfig("./config.json");
|
|
380
|
-
|
|
381
|
-
// Propagate the file system error directly if config read fails
|
|
382
|
-
if (isErr(configResult)) {
|
|
383
|
-
return configResult;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// If config is read, but is invalid, return a *different* kind of error
|
|
387
|
-
const validationResult = validateConfig(configResult.data);
|
|
388
|
-
if (isErr(validationResult)) {
|
|
389
|
-
return validationResult;
|
|
390
|
-
}
|
|
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
|
+
});
|
|
391
192
|
|
|
392
|
-
|
|
393
|
-
|
|
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
|
+
});
|
|
394
203
|
```
|
|
395
204
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
---
|
|
399
|
-
|
|
400
|
-
## Basic Usage
|
|
401
|
-
|
|
402
|
-
Now that you understand how to handle Result values and the TaggedError structure, let's see complete examples that combine both concepts:
|
|
205
|
+
### Service Layer Example
|
|
403
206
|
|
|
404
|
-
```
|
|
405
|
-
import { Result, Ok,
|
|
406
|
-
import {
|
|
407
|
-
|
|
408
|
-
// --- Example 1: A Safe Division Function ---
|
|
207
|
+
```typescript
|
|
208
|
+
import { Result, Ok, tryAsync } from "wellcrafted/result";
|
|
209
|
+
import { createTaggedError } from "wellcrafted/error";
|
|
409
210
|
|
|
410
|
-
//
|
|
411
|
-
|
|
211
|
+
// Define service-specific errors
|
|
212
|
+
const { ValidationError, ValidationErr } = createTaggedError("ValidationError");
|
|
213
|
+
const { DatabaseError, DatabaseErr } = createTaggedError("DatabaseError");
|
|
412
214
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (denominator === 0) {
|
|
416
|
-
return Err({
|
|
417
|
-
name: "MathError",
|
|
418
|
-
message: "Cannot divide by zero.",
|
|
419
|
-
context: { numerator, denominator },
|
|
420
|
-
cause: undefined
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
return Ok(numerator / denominator);
|
|
424
|
-
}
|
|
215
|
+
type ValidationError = ReturnType<typeof ValidationError>;
|
|
216
|
+
type DatabaseError = ReturnType<typeof DatabaseError>;
|
|
425
217
|
|
|
426
|
-
//
|
|
427
|
-
|
|
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
|
+
}
|
|
428
230
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
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
|
+
},
|
|
434
240
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const data = JSON.parse(json);
|
|
444
|
-
if (typeof data.name !== "string") {
|
|
445
|
-
return Err({
|
|
446
|
-
name: "ParseError",
|
|
447
|
-
message: "User object must have a name property of type string.",
|
|
448
|
-
context: { receivedValue: data.name },
|
|
449
|
-
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
|
+
})
|
|
450
249
|
});
|
|
451
250
|
}
|
|
452
|
-
|
|
453
|
-
} catch (e) {
|
|
454
|
-
return Err({
|
|
455
|
-
name: "ParseError",
|
|
456
|
-
message: "Invalid JSON provided.",
|
|
457
|
-
context: { rawString: json },
|
|
458
|
-
cause: e,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
251
|
+
};
|
|
461
252
|
}
|
|
462
253
|
|
|
463
|
-
//
|
|
464
|
-
|
|
254
|
+
// Export type for the service
|
|
255
|
+
export type UserService = ReturnType<typeof createUserService>;
|
|
465
256
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
} else {
|
|
469
|
-
// `userResult.error` is a fully-typed ParseError object
|
|
470
|
-
console.error(`Error (${userResult.error.name}): ${userResult.error.message}`);
|
|
471
|
-
console.log("Context:", userResult.error.context);
|
|
472
|
-
}
|
|
257
|
+
// Create a live instance (dependency injection at build time)
|
|
258
|
+
export const UserServiceLive = createUserService(databaseInstance);
|
|
473
259
|
```
|
|
474
260
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
## Wrapping Functions That Throw
|
|
261
|
+
## Why wellcrafted?
|
|
478
262
|
|
|
479
|
-
|
|
263
|
+
JavaScript's `try-catch` has fundamental problems:
|
|
480
264
|
|
|
481
|
-
|
|
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)
|
|
482
269
|
|
|
483
|
-
|
|
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
|
|
484
275
|
|
|
485
|
-
|
|
486
|
-
import { trySync, Result } from "wellcrafted/result";
|
|
487
|
-
import { type TaggedError } from "wellcrafted/error";
|
|
276
|
+
## Service Pattern Best Practices
|
|
488
277
|
|
|
489
|
-
|
|
278
|
+
Based on real-world usage, here's the recommended pattern for creating services with wellcrafted:
|
|
490
279
|
|
|
491
|
-
|
|
492
|
-
return trySync({
|
|
493
|
-
try: () => JSON.parse(raw),
|
|
494
|
-
mapError: (error) => ({
|
|
495
|
-
name: "ParseError",
|
|
496
|
-
message: "Failed to parse JSON",
|
|
497
|
-
context: { raw },
|
|
498
|
-
cause: error
|
|
499
|
-
})
|
|
500
|
-
});
|
|
501
|
-
}
|
|
280
|
+
### Factory Function Pattern
|
|
502
281
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
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
|
+
});
|
|
525
303
|
}
|
|
526
|
-
|
|
304
|
+
|
|
305
|
+
isRecording = true;
|
|
306
|
+
return Ok(undefined);
|
|
527
307
|
},
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
+
};
|
|
535
322
|
}
|
|
536
323
|
|
|
537
|
-
|
|
538
|
-
|
|
324
|
+
// 3. Export type
|
|
325
|
+
export type RecorderService = ReturnType<typeof createRecorderService>;
|
|
539
326
|
|
|
540
|
-
|
|
327
|
+
// 4. Create singleton instance
|
|
328
|
+
export const RecorderServiceLive = createRecorderService();
|
|
329
|
+
```
|
|
541
330
|
|
|
542
|
-
|
|
331
|
+
### Platform-Specific Services
|
|
543
332
|
|
|
544
|
-
|
|
333
|
+
For services that need different implementations per platform:
|
|
545
334
|
|
|
546
|
-
|
|
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
|
+
};
|
|
547
341
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const content = await fs.readFile(path, 'utf-8');
|
|
554
|
-
return content;
|
|
342
|
+
// desktop.ts
|
|
343
|
+
export function createFileServiceDesktop(): FileService {
|
|
344
|
+
return {
|
|
345
|
+
async readFile(path) {
|
|
346
|
+
// Desktop implementation using Node.js APIs
|
|
555
347
|
},
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
cause: error
|
|
561
|
-
})
|
|
562
|
-
});
|
|
348
|
+
async writeFile(path, content) {
|
|
349
|
+
// Desktop implementation
|
|
350
|
+
}
|
|
351
|
+
};
|
|
563
352
|
}
|
|
564
353
|
|
|
565
|
-
//
|
|
566
|
-
function
|
|
567
|
-
return
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
validateConfig(parsed); // throws if invalid
|
|
571
|
-
return parsed as Config;
|
|
354
|
+
// web.ts
|
|
355
|
+
export function createFileServiceWeb(): FileService {
|
|
356
|
+
return {
|
|
357
|
+
async readFile(path) {
|
|
358
|
+
// Web implementation using File API
|
|
572
359
|
},
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
cause: error
|
|
578
|
-
})
|
|
579
|
-
});
|
|
360
|
+
async writeFile(path, content) {
|
|
361
|
+
// Web implementation
|
|
362
|
+
}
|
|
363
|
+
};
|
|
580
364
|
}
|
|
365
|
+
|
|
366
|
+
// index.ts - runtime selection
|
|
367
|
+
export const FileServiceLive = typeof window !== 'undefined'
|
|
368
|
+
? createFileServiceWeb()
|
|
369
|
+
: createFileServiceDesktop();
|
|
581
370
|
```
|
|
582
371
|
|
|
583
|
-
|
|
372
|
+
## Common Use Cases
|
|
584
373
|
|
|
585
|
-
|
|
374
|
+
<details>
|
|
375
|
+
<summary><b>API Route Handler</b></summary>
|
|
586
376
|
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// For trySync
|
|
605
|
-
function validateEmail(email: string) {
|
|
606
|
-
return trySync({
|
|
607
|
-
try: () => {
|
|
608
|
-
if (!email.includes('@')) {
|
|
609
|
-
throw new Error('Invalid email format');
|
|
610
|
-
}
|
|
611
|
-
return email.toLowerCase();
|
|
612
|
-
},
|
|
613
|
-
mapError: (error): ValidationError => ({ // 👈 Annotate return type
|
|
614
|
-
name: "ValidationError",
|
|
615
|
-
message: "Email validation failed",
|
|
616
|
-
context: { email },
|
|
617
|
-
cause: error
|
|
618
|
-
})
|
|
619
|
-
});
|
|
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);
|
|
620
391
|
}
|
|
621
392
|
```
|
|
393
|
+
</details>
|
|
622
394
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
- Avoid using `as const` - always map to proper TaggedError objects
|
|
626
|
-
- Choose explicit generics for clarity, or return type annotation for brevity
|
|
627
|
-
- The important thing is ensuring type safety for your error handling
|
|
628
|
-
|
|
629
|
-
---
|
|
630
|
-
|
|
631
|
-
## Partitioning Results
|
|
632
|
-
|
|
633
|
-
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.
|
|
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
|
-
const config = getCommandConfig(command.id);
|
|
654
|
-
if (!config) return; // Early return if config missing
|
|
655
|
-
return executeCommand({ command, config });
|
|
656
|
-
})
|
|
657
|
-
.filter((result) => result !== undefined) // Remove undefined values
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
const { oks, errs } = partitionResults(results);
|
|
661
|
-
|
|
662
|
-
// Handle all errors at once
|
|
663
|
-
if (errs.length > 0) {
|
|
664
|
-
const errorMessages = errs.map(({ error }) => error.message).join(', ');
|
|
665
|
-
showNotification(`${errs.length} operations failed: ${errorMessages}`);
|
|
666
|
-
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);
|
|
667
416
|
}
|
|
668
|
-
|
|
669
|
-
return oks.map(ok => ok.data); // Return processed content
|
|
670
417
|
```
|
|
418
|
+
</details>
|
|
671
419
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
```ts
|
|
675
|
-
import { tryAsync, partitionResults } from "wellcrafted/result";
|
|
676
|
-
import { type TaggedError } from "wellcrafted/error";
|
|
677
|
-
import * as fs from 'fs/promises';
|
|
678
|
-
|
|
679
|
-
type FileError = TaggedError<"FileError">;
|
|
680
|
-
|
|
681
|
-
async function processFiles(filePaths: string[]) {
|
|
682
|
-
// Map each file path to a Result-returning operation
|
|
683
|
-
const results = await Promise.all(
|
|
684
|
-
filePaths
|
|
685
|
-
.map((path) => {
|
|
686
|
-
if (!path.endsWith('.txt')) return; // Skip non-text files
|
|
687
|
-
return tryAsync<string, FileError>({
|
|
688
|
-
try: async () => {
|
|
689
|
-
const content = await fs.readFile(path, 'utf-8');
|
|
690
|
-
return content.toUpperCase(); // Process the content
|
|
691
|
-
},
|
|
692
|
-
mapError: (error) => ({
|
|
693
|
-
name: "FileError",
|
|
694
|
-
message: "Failed to process file",
|
|
695
|
-
context: { path },
|
|
696
|
-
cause: error
|
|
697
|
-
})
|
|
698
|
-
});
|
|
699
|
-
})
|
|
700
|
-
.filter((result) => result !== undefined)
|
|
701
|
-
);
|
|
702
|
-
|
|
703
|
-
const { oks, errs } = partitionResults(results);
|
|
420
|
+
<details>
|
|
421
|
+
<summary><b>React Hook</b></summary>
|
|
704
422
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
+
}
|
|
710
438
|
});
|
|
711
|
-
}
|
|
439
|
+
}, [id]);
|
|
712
440
|
|
|
713
|
-
|
|
714
|
-
if (oks.length > 0) {
|
|
715
|
-
console.log(`Successfully processed ${oks.length} files`);
|
|
716
|
-
return oks.map(ok => ok.data); // Return processed content
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
return [];
|
|
441
|
+
return state;
|
|
720
442
|
}
|
|
721
443
|
```
|
|
444
|
+
</details>
|
|
722
445
|
|
|
723
|
-
|
|
446
|
+
## Comparison with Alternatives
|
|
724
447
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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 |
|
|
731
455
|
|
|
732
456
|
## API Reference
|
|
733
457
|
|
|
734
|
-
###
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
| `trySync()` | Wrap throwing function | `trySync({ try: () => JSON.parse(str) })` |
|
|
743
|
-
| `tryAsync()` | Wrap async throwing function | `tryAsync({ try: () => fetch(url) })` |
|
|
744
|
-
| `partitionResults()` | Split Results into oks/errs | `const { oks, errs } = partitionResults(results)` |
|
|
745
|
-
|
|
746
|
-
### Detailed API
|
|
747
|
-
|
|
748
|
-
#### Types
|
|
749
|
-
- **`Result<T, E>`**: The core union type, representing `Ok<T> | Err<E>`.
|
|
750
|
-
- **`Ok<T>`**: Represents a success. Contains `{ data: T; error: null; }`.
|
|
751
|
-
- **`Err<E>`**: Represents a failure. Contains `{ data: null; error: E; }`.
|
|
752
|
-
- **`BaseError` / `TaggedError<T>`**: Helpers for creating a structured error system.
|
|
753
|
-
|
|
754
|
-
#### Core Result Functions
|
|
755
|
-
- **`Ok(data)`**: Creates a success `Result`.
|
|
756
|
-
- **`Err(error)`**: Creates a failure `Result`.
|
|
757
|
-
- **`isOk(result)`**: Type guard. Returns `true` if the result is an `Ok` variant.
|
|
758
|
-
- **`isErr(result)`**: Type guard. Returns `true` if the result is an `Err` variant.
|
|
759
|
-
- **`unwrap(result)`**: Unwraps a `Result`, returning data on `Ok` or throwing error on `Err`.
|
|
760
|
-
- **`resolve(value)`**: Resolves a value that may or may not be a `Result`, returning the final value or throwing on `Err`.
|
|
761
|
-
- **`isResult(value)`**: Type guard. Returns `true` if a value has the shape of a `Result`.
|
|
762
|
-
|
|
763
|
-
#### Async/Sync Wrappers
|
|
764
|
-
- **`trySync({ try, mapError })`**: Wraps a synchronous function that may throw.
|
|
765
|
-
- **`tryAsync({ try, mapError })`**: Wraps an asynchronous function that may throw or reject.
|
|
766
|
-
|
|
767
|
-
#### Error Utilities
|
|
768
|
-
- **`extractErrorMessage(error)`**: Safely extracts a string message from any error value.
|
|
769
|
-
|
|
770
|
-
#### Utility Functions
|
|
771
|
-
- **`partitionResults(results)`**: Partitions an array of Results into separate arrays of `Ok` and `Err` variants.
|
|
772
|
-
|
|
773
|
-
---
|
|
774
|
-
|
|
775
|
-
## Design Philosophy
|
|
776
|
-
|
|
777
|
-
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.
|
|
778
|
-
|
|
779
|
-
### 1. Embrace JavaScript Primitives
|
|
780
|
-
|
|
781
|
-
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.
|
|
782
|
-
|
|
783
|
-
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
|
|
784
466
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
|
788
472
|
|
|
789
|
-
|
|
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
|
|
790
477
|
|
|
791
|
-
|
|
478
|
+
## Learn More
|
|
792
479
|
|
|
793
|
-
|
|
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)
|
|
794
483
|
|
|
795
|
-
|
|
484
|
+
## License
|
|
796
485
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
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.
|
|
800
|
-
|
|
801
|
-
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.
|
|
802
|
-
|
|
803
|
-
### 4. Serialization-First
|
|
804
|
-
|
|
805
|
-
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.
|
|
806
|
-
|
|
807
|
-
### 5. Opinionated yet Flexible
|
|
808
|
-
|
|
809
|
-
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.
|
|
810
|
-
|
|
811
|
-
## Inspirations and Relationship to Effect-TS
|
|
812
|
-
|
|
813
|
-
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.
|
|
814
|
-
|
|
815
|
-
However, this library represents a different set of trade-offs and priorities, based on a few key disagreements with the Effect-TS approach:
|
|
816
|
-
|
|
817
|
-
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.
|
|
818
|
-
|
|
819
|
-
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.
|
|
820
|
-
|
|
821
|
-
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
|
|
822
487
|
|
|
823
488
|
---
|
|
824
489
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
### Why `{ data, error }` instead of a boolean flag like `{ ok: boolean, ... }`?
|
|
828
|
-
|
|
829
|
-
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:
|
|
830
|
-
|
|
831
|
-
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.
|
|
832
|
-
|
|
833
|
-
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.
|
|
834
|
-
|
|
835
|
-
**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.
|
|
836
|
-
|
|
837
|
-
### What's the difference between an `Err` variant and an `error` value?
|
|
838
|
-
|
|
839
|
-
This is a key distinction in the library's terminology:
|
|
840
|
-
|
|
841
|
-
- **`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.
|
|
842
|
-
|
|
843
|
-
- **`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`.
|
|
844
|
-
|
|
845
|
-
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.
|
|
846
|
-
|
|
847
|
-
### Why doesn't this library include an `Option<T>` type?
|
|
848
|
-
|
|
849
|
-
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.**
|
|
850
|
-
|
|
851
|
-
A custom `Option<T>` type would add a layer of abstraction that is largely unnecessary. Instead, you can and should use:
|
|
852
|
-
|
|
853
|
-
1. **Union Types with `null`**: Simply type your value as `T | null`. This is the idiomatic way to represent an optional value in TypeScript.
|
|
854
|
-
|
|
855
|
-
2. **Optional Chaining (`?.`)**: Safely access nested properties of an object that might be null or undefined.
|
|
856
|
-
```ts
|
|
857
|
-
const street = user?.address?.street; // Returns undefined if user or address is null/undefined
|
|
858
|
-
```
|
|
859
|
-
|
|
860
|
-
3. **Nullish Coalescing (`??`)**: Provide a default value for a `null` or `undefined` expression.
|
|
861
|
-
```ts
|
|
862
|
-
const displayName = user.name ?? "Guest";
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
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.
|