@exodus/errors 2.0.2 β†’ 2.1.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/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [2.1.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@2.1.0...@exodus/errors@2.1.1) (2025-05-08)
7
+
8
+ ### Bug Fixes
9
+
10
+ - fix: remove `void` keyword (#12267)
11
+
12
+ ## [2.1.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@2.0.2...@exodus/errors@2.1.0) (2025-04-28)
13
+
14
+ ### Features
15
+
16
+ - feat: introduce `captureStackTrace` util (#12042)
17
+
6
18
  ## [2.0.2](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@2.0.1...@exodus/errors@2.0.2) (2025-02-27)
7
19
 
8
20
  ### Bug Fixes
package/README.md CHANGED
@@ -12,3 +12,24 @@ try {
12
12
  sendToErrorTrackingServer(parseStackTrace(e))
13
13
  }
14
14
  ```
15
+
16
+ ## Troubleshooting
17
+
18
+ ### `parseStackTrace` returns undefined stack trace?
19
+
20
+ That likely means that something accesses `error.stack` _before_ the `parseStackTrace` had a chance to apply the custom handler. This could be React, a high-level error handler, or any other framework. `error.stack` is computed only on the first access, so the custom handler (`prepareStackTrace`) won’t be called on subsequent attempts (see the `stack.test.js` for a quick demo).
21
+
22
+ If you can identify the exact place where `.stack` is accessed, consider capturing the stack trace explicitly like this:
23
+
24
+ ```javascript
25
+ import { parseStackTrace, captureStackTrace } from '@exodus/errors'
26
+
27
+ try {
28
+ // the devil's work
29
+ } catch (e) {
30
+ captureStackTrace(e)
31
+ void e.stack // Intentionally access the property to "break" it here.
32
+ sendToErrorTrackingServer(parseStackTrace(e))
33
+ // πŸŽ‰ Congrats β€” you just (hopefully) saved hours of debugging! `parseStackTrace` now works because the stack was captured explicitly above.
34
+ }
35
+ ```
package/lib/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default as sanitizeErrorMessage } from './sanitize.js';
2
- export { default as parseStackTrace } from './stack.js';
2
+ export { default as parseStackTrace, captureStackTrace } from './stack.js';
3
3
  export * from './safe-error.js';
package/lib/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default as sanitizeErrorMessage } from './sanitize.js';
2
- export { default as parseStackTrace } from './stack.js';
2
+ export { default as parseStackTrace, captureStackTrace } from './stack.js';
3
3
  export * from './safe-error.js';
package/lib/stack.d.ts CHANGED
@@ -1,3 +1,15 @@
1
1
  import type { Frame } from './types.js';
2
+ export declare function captureStackTrace(err: Error): void;
2
3
  export declare function stackFramesToString(frames?: Frame[]): string | undefined;
4
+ /**
5
+ * If this function returns undefined, that likely means `error.stack` has been already accessed.
6
+ * Consider calling `captureStackTrace` first to capture the customized stack trace, e.g.,
7
+ *
8
+ * ```ts
9
+ * captureStackTrace(error)
10
+ * console.log(error.stack)
11
+ * ...
12
+ * const safeError = SafeError.from(error) // Works even if `error.stack` has been accessed.
13
+ * ```
14
+ */
3
15
  export default function parseStackTraceNatively(err: Error): Frame[] | undefined;
package/lib/stack.js CHANGED
@@ -1,21 +1,14 @@
1
- export function stackFramesToString(frames) {
2
- if (frames === undefined) {
1
+ const stackCache = new WeakMap();
2
+ export function captureStackTrace(err) {
3
+ if (stackCache.has(err)) {
3
4
  return;
4
5
  }
5
- return frames
6
- .map((frame) => {
7
- const { function: fn, file, line, column } = frame;
8
- return ` at ${fn || 'unknownFn'}${file ? ` (${file}${line === null ? '' : `:${line}:${column}`})` : ''}`;
9
- })
10
- .join('\n');
11
- }
12
- export default function parseStackTraceNatively(err) {
13
- const { prepareStackTrace } = Error;
14
- let stack;
6
+ const { prepareStackTrace: originalPrepareStackTrace } = Error;
7
+ let structuredStack;
15
8
  let calledOn;
16
- Error.prepareStackTrace = (e, callSites) => {
17
- calledOn = e;
18
- stack = callSites.map((trace) => ({
9
+ Error.prepareStackTrace = (err, callSites) => {
10
+ calledOn = err;
11
+ structuredStack = callSites.map((trace) => ({
19
12
  function: trace.getFunctionName(),
20
13
  method: trace.getMethodName(),
21
14
  file: trace.getFileName(),
@@ -25,15 +18,55 @@ export default function parseStackTraceNatively(err) {
25
18
  async: trace.isAsync?.(),
26
19
  toplevel: trace.isToplevel(),
27
20
  }));
21
+ // Let V8 continue to build the default `.stack` string
22
+ if (originalPrepareStackTrace)
23
+ return originalPrepareStackTrace(err, callSites);
24
+ // Non V8 case (e.g. Hermes).
25
+ return err.stack;
28
26
  };
29
27
  try {
28
+ // do NOT add "void", it will remove the whole line in a tragic transpilation accident
30
29
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
31
30
  err.stack; // trigger prepareStackTrace
32
31
  }
33
32
  finally {
34
- Error.prepareStackTrace = prepareStackTrace;
33
+ Error.prepareStackTrace = originalPrepareStackTrace;
35
34
  }
36
35
  // @ts-expect-error calledOn gets assigned in prepareStackTrace
37
- if (calledOn === err && Array.isArray(stack))
38
- return stack;
36
+ if (calledOn === err && Array.isArray(structuredStack)) {
37
+ stackCache.set(err, structuredStack);
38
+ }
39
+ else {
40
+ // This helps to avoid calling `captureStackTrace` multiple times on the same error.
41
+ stackCache.set(err, undefined);
42
+ }
43
+ }
44
+ export function stackFramesToString(frames) {
45
+ if (frames === undefined) {
46
+ return;
47
+ }
48
+ return frames
49
+ .map((frame) => {
50
+ const { function: fn, file, line, column } = frame;
51
+ return ` at ${fn || 'unknownFn'}${file ? ` (${file}${line === null ? '' : `:${line}:${column}`})` : ''}`;
52
+ })
53
+ .join('\n');
54
+ }
55
+ /**
56
+ * If this function returns undefined, that likely means `error.stack` has been already accessed.
57
+ * Consider calling `captureStackTrace` first to capture the customized stack trace, e.g.,
58
+ *
59
+ * ```ts
60
+ * captureStackTrace(error)
61
+ * console.log(error.stack)
62
+ * ...
63
+ * const safeError = SafeError.from(error) // Works even if `error.stack` has been accessed.
64
+ * ```
65
+ */
66
+ export default function parseStackTraceNatively(err) {
67
+ if (stackCache.has(err)) {
68
+ return stackCache.get(err);
69
+ }
70
+ captureStackTrace(err);
71
+ return stackCache.get(err);
39
72
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exodus/errors",
3
3
  "type": "module",
4
- "version": "2.0.2",
4
+ "version": "2.1.1",
5
5
  "description": "Utilities for error handling in client code, such as sanitization",
6
6
  "author": "Exodus Movement, Inc.",
7
7
  "repository": {
@@ -38,11 +38,10 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@exodus/errors-fixture": "^1.0.0",
41
- "@types/minimalistic-assert": "^1.0.1",
42
- "hermes-engine-cli": "^0.12.0"
41
+ "@types/minimalistic-assert": "^1.0.1"
43
42
  },
44
43
  "publishConfig": {
45
44
  "access": "public"
46
45
  },
47
- "gitHead": "7fa27377d258917916c478eaa84e2495a23467be"
46
+ "gitHead": "5d915aed4f43c8b9377e87c902347aac778391d2"
48
47
  }