Skip to content

Commit 8bf596f

Browse files
authored
Revise README for error handling and tracing
Updated error handling section with improved explanations and examples, including changes to terminology and formatting for clarity.
1 parent d707dfc commit 8bf596f

1 file changed

Lines changed: 81 additions & 21 deletions

File tree

packages/core/README.md

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,9 @@ Throwing and catching:
4747
import {AppError} from './errors';
4848

4949
throw AppError.NotFound(123);
50-
51-
// ...later
52-
if (e instanceof Error) {
53-
if (AppError.is(e)) {
54-
// narrowed to AppError family
55-
}
56-
}
5750
```
5851

59-
## 🧪 Adopting external errors
52+
## 🛠️ Adopting external errors
6053

6154
### enroll (one-to-one)
6255

@@ -72,7 +65,7 @@ class MyLegacyError extends Error {
7265
const ExAppError = AppError.enroll(MyLegacyError, AppError.NotFound, (e) => [Number(e.legacyId)]);
7366

7467
const err = ExAppError.from(new MyLegacyError("123"));
75-
// err is typed as AppError.NotFound and carries payload [123]
68+
// err is typed as ExAppError.NotFound and carries payload [123]
7669
```
7770

7871
### bridge (conditional / one-to-many)
@@ -94,42 +87,110 @@ const ExAppError = AppError.bridge(HTTPException, (e, cases) => {
9487

9588
### from — the type-safe gateway
9689

97-
`from` only accepts error types you have enrolled or bridged. If an unenrolled error is passed, TypeScript will flag it.
90+
`from` only accepts error types you have enrolled or bridged. If an `unenrolled or bridged` error is passed, TypeScript will flag it.
9891

9992
```typescript
10093
try {
10194
// ...
10295
} catch (e: unknown) {
10396
if (e instanceof MyLegacyError) {
104-
const error = ExAppError.from(e); // typed
97+
const error = ExAppError.from(e); // error is typed as ExAppError.NotFound
10598
}
10699
}
107100
```
108101

109-
## 🎯 Deterministic Tracing with `.with()`
102+
## 🧪 Deterministic Tracing: The "Callback-Local" Anchor
103+
104+
In JavaScript, asynchronous stack traces are notoriously fragile. When you wrap errors inside a callback
105+
like [neverthrow](https://github.com/supermacro/neverthrow) 's `ResultAsync.fromPromise`, the stack trace often points
106+
to
107+
the library's internal dispatchers rather than your business logic.
108+
109+
```ts
110+
// location: project/mcp/client.ts
111+
import {ResultAsync} from 'neverthrow';
112+
import {MCPError} from "./error";
113+
114+
function connectTo(url: string) {
115+
return ResultAsync.fromPromise(
116+
client.connect(url),
117+
(_err) => {
118+
// 🚨 THE ISSUE:
119+
// When this callback is executed, the physical execution flow
120+
// is already deep inside neverthrow's internal logic.
121+
return MCPError.CONNECTION_FAILED(url);
122+
}
123+
);
124+
}
125+
126+
const result = await connectTo("ws://localhost:3000");
127+
if (result.isErr()) {
128+
console.log(result.error.stack);
129+
}
130+
```
131+
132+
The Resulting "Messy" Stack Trace:
133+
134+
```shell
135+
Error: Failed to connect: "ws://localhost:3000"
136+
at /project/node_modules/neverthrow/dist/index.cjs.js:106:34 <-- 🛑 Useless! Internal library code.
137+
at processTicksAndRejections (native)
138+
```
110139

111-
When creating errors inside callbacks (for example, `ResultAsync.fromPromise`), V8 may capture a stack trace that
112-
starts inside the callback library rather than your code. Calling `.with()` within the callback preserves the stack
113-
trace at the point you created the error.
140+
You’ll notice the top frames point to internal files of `neverthrow`, making it impossible to see where your business
141+
logic actually failed.
142+
143+
### The "Magic" of `.with()`
144+
145+
By using the chainable `.with()` method, you force the V8 engine to capture the stack trace at the **exact
146+
moment** of failure within your callback.
114147

115148
```typescript
149+
// 🟢 The ResultAsync Way (Best Practice)
150+
// location: project/mcp/client.ts
116151
return ResultAsync.fromPromise(
117152
client.connect(url),
118-
(error) => AppError.ConnectionError(url).with({cause: error})
153+
(error) => MCPError.CONNECTION_FAILED(url).with({cause: error})
119154
);
120155
```
121156

122-
`.with()` is chainable and usually returns `this` — its value is the captured error; the main purpose is to "anchor"
123-
stack capture at the callsite.
157+
now the stack trace points to your business logic:
158+
159+
```shell
160+
Error: Failed to connect: "ws://localhost:3000"
161+
at /project/mcp/client.ts:15:11 <-- 🟢 Useful! Business code.
162+
at /project/node_modules/neverthrow/dist/index.cjs.js:106:34
163+
at processTicksAndRejections (native)
164+
```
165+
166+
🎯 The "Crime Scene": Callback Freedom
167+
With the .with() anchor, you are finally free to nest your business logic deep within any callback without fear of
168+
losing context.
169+
170+
To be honest, at the implementation level, `.with()` is almost a "no-op" (it just returns `this`). However, in the
171+
physical world of V8 and asynchronous microtasks, it acts as a **Quantum Observer**.
172+
173+
#### Why "It Just Works":
174+
175+
- **Microtask Locking**: By calling `.with()` immediately within your callback, you force the engine to interact with
176+
the error object before the current microtask ends. This "extra step" effectively nails the stack trace to the
177+
physical floor before the asynchronous execution context evaporates.
178+
- **Optimization Barrier**: It prevents the JIT compiler from over-optimizing (inlining) the factory call into the
179+
library's internal dispatchers, preserving the "Crime Scene" frames.
180+
181+
> **Man, what can I say?** We can't fully explain why the ghost of the stack trace stays longer when you call `.with()`,
182+
> but the experimental evidence is clear: **It just works.** Call it, and you'll never have to guess where your errors
183+
> came from again.
124184
125185
## API Overview
126186

127187
- That(schema) -> family factory
128-
- err.is(err) -> type guard
188+
- error.is(err) -> type guard check
129189
- family.enroll(ExternalClass, familyCase, transformer?) → returns an extended family
130190
- family.bridge(ExternalClass, dispatcher) → returns an extended family
131191
- family.from(externalError) → transforms to a family case
132192
- error.with({ cause }) → attach cause and anchor stack trace
193+
- ThatError → generic thiserror instance type
133194

134195
## Adapter: pino
135196

@@ -141,8 +202,7 @@ See `packages/core/__tests__` for concrete usage and test assertions that demons
141202

142203
## Contributing
143204

144-
Run the workspace install and package tests from the repository root (see the root README). Each package may include additional
145-
contributing instructions.
205+
Run the workspace install and package tests from the repository root (see the root README).
146206

147207
## License
148208

0 commit comments

Comments
 (0)