@@ -47,16 +47,9 @@ Throwing and catching:
4747import {AppError } from ' ./errors' ;
4848
4949throw 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 {
7265const ExAppError = AppError .enroll (MyLegacyError , AppError .NotFound , (e ) => [Number (e .legacyId )]);
7366
7467const 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
10093try {
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
116151return 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