@exodus/errors 3.0.0 → 3.1.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/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
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
+ ## [3.1.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.0.1...@exodus/errors@3.1.0) (2025-08-04)
7
+
8
+ ### Features
9
+
10
+ - feat: support interpolating non-dynamic safeString into safeString on parse (#13003)
11
+
12
+ ## [3.0.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.0.0...@exodus/errors@3.0.1) (2025-07-08)
13
+
14
+ **Note:** Version bump only for package @exodus/errors
15
+
6
16
  ## [3.0.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@2.1.1...@exodus/errors@3.0.0) (2025-05-16)
7
17
 
8
18
  ### ⚠ BREAKING CHANGES
package/README.md CHANGED
@@ -3,33 +3,102 @@
3
3
  ## Usage
4
4
 
5
5
  ```javascript
6
- import { sanitizeErrorMessage, parseStackTrace } from '@exodus/errors'
6
+ import { SafeError } from '@exodus/errors'
7
7
 
8
8
  try {
9
- // the devil's work
9
+ throw new Error('Something went wrong')
10
10
  } catch (e) {
11
- console.error(sanitizeErrorMessage(e.message))
12
- sendToErrorTrackingServer(parseStackTrace(e))
11
+ const safeError = SafeError.from(e)
12
+
13
+ // It's now safe to report or log the error, even to a remote server.
14
+ console.error({
15
+ name: safeError.name, // Sanitized error name.
16
+ code: safeError.code, // Optional error code (if present).
17
+ hint: safeError.hint, // Optional error hint (if present).
18
+ stack: safeError.stack, // Sanitized stack trace.
19
+ timestamp: safeError.timestamp, // When the error occurred.
20
+ })
21
+ }
22
+ ```
23
+
24
+ ## FAQ
25
+
26
+ ### Why using SafeError instead of built-in Errors?
27
+
28
+ In large codebases, errors can be thrown from anywhere, making it impossible to audit every error message for sensitive information. A single error containing sensitive data could potentially expose user information. Centralizing error handling with `SafeError` makes it possible to enforce security and consistency across the board, by ensuring:
29
+
30
+ 1. **Controlled Error Flow**: All errors go through a single, well-tested sanitization layer before they hit error tracking systems.
31
+ 2. **Enforceable Security**: Error handling can be owned through codeowners and covered by tests, so nothing slips through unnoticed.
32
+
33
+ In addition to enforcing these practices, `SafeError` includes a few key design decisions that make it safer and more reliable than native Error objects:
34
+
35
+ 1. **Message Sanitization**: The `message` property from built-in Errors is intentionally omitted as it often contains sensitive information. Instead, a `hint` property is used that contains only sanitized, non-sensitive information.
36
+ 2. **Native Stack Parsing**: The library uses the [`Error.prepareStackTrace` API](https://v8.dev/docs/stack-trace-api) to parse stack traces, providing consistent and reliable stack trace information across different JavaScript environments.
37
+ 3. **Immutability**: Once created, a `SafeError` instance cannot be modified, preventing tampering with error data.
38
+ 4. **Safe Serialization**: The `toJSON` method ensures safe serialization for logging or sending to error tracking services.
39
+
40
+ ### Why not redacting Error messages?
41
+
42
+ Parsing/sanitization of error messages is unreliable and the cost of failure is potential loss of user funds and permanent reputation damage.
43
+
44
+ ### Why not use the built-in `.stack` Error property?
45
+
46
+ Unfortunately, the built-in `.stack` property is mutable and outside of our control. Instead, we use the `Error.prepareStackTrace` API, which enables us to make sure we access the actual call stack and not a cached `err.stack` value that may have already been consumed and modified. We then parse it into a structured format that we can safely sanitize and control. This approach provides consistent, reliable stack traces across different environments (currently supporting both V8 and Hermes).
47
+
48
+ ## Recipes
49
+
50
+ > [!TIP]
51
+ > Before diving into the recipes, you might want to get familiar with what a 'Safe String' is: https://github.com/ExodusMovement/exodus-hydra/tree/master/libraries/safe-string
52
+
53
+ ### I want to preserve server errors in Safe Errors
54
+
55
+ Do you control the server? If so, better send short error codes from your server instead. `err.code` will [be passed through](https://github.com/ExodusOSS/hydra/blob/master/libraries/errors/src/safe-error.ts#L32) SafeError coercion.
56
+
57
+ If you do NOT control the server, and you know the specific error messages returned by the server, you can predefine them wrapped in `safeString`:
58
+
59
+ ```js
60
+ import { safeString } from '@exodus/safe-string'
61
+
62
+ // From: https://github.com/ethereum/go-ethereum/blob/master/core/txpool/errors.go.
63
+ export const KnownErrors = new Set([
64
+ safeString`already known`,
65
+ safeString`invalid sender`,
66
+ safeString`transaction underpriced`,
67
+ ])
68
+ ```
69
+
70
+ You can now handle failed requests like this:
71
+
72
+ ```js
73
+ import { safeString } from '@exodus/safe-string`
74
+ import { KnownErrors } from './errors.js'
75
+
76
+ const response = await this.request(request)
77
+ const error = response?.error
78
+
79
+ if (error) {
80
+ const message = KnownErrors.get(msg) ?? safeString`unknown error`
81
+ throw new Error(safeString`Bad rpc response: ${message}`)
13
82
  }
14
83
  ```
15
84
 
16
85
  ## Troubleshooting
17
86
 
18
- ### `parseStackTrace` returns undefined stack trace?
87
+ ### A SafeError instance returns `undefined` stack trace?
19
88
 
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).
89
+ That likely means that something accesses `error.stack` _before_ the Safe Error constructor had a chance to apply the custom `Error.prepareStackTrace` 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 won’t be called on subsequent attempts (see the `stack.test.js` for a quick demo).
21
90
 
22
91
  If you can identify the exact place where `.stack` is accessed, consider capturing the stack trace explicitly like this:
23
92
 
24
93
  ```javascript
25
- import { parseStackTrace, captureStackTrace } from '@exodus/errors'
94
+ import { captureStackTrace, SafeError } from '@exodus/errors'
26
95
 
27
96
  try {
28
97
  // the devil's work
29
98
  } catch (e) {
30
99
  captureStackTrace(e)
31
100
  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.
101
+ SafeError.from(e).stack // A non-empty string!
102
+ // 🎉 Congrats — you just (hopefully) saved hours of debugging! The custom stack trace parsing logic now works because the stack was captured explicitly above.
34
103
  }
35
104
  ```
@@ -12,16 +12,16 @@ declare const commonErrors: readonly [{
12
12
  readonly normalized: "Cannot convert undefined to object";
13
13
  }, {
14
14
  readonly pattern: RegExp;
15
- readonly normalized: "Cannot read property/properties of null";
15
+ readonly normalized: "Cannot read properties of null";
16
16
  }, {
17
17
  readonly pattern: RegExp;
18
- readonly normalized: "Cannot read property/properties of undefined";
18
+ readonly normalized: "Cannot read properties of undefined";
19
19
  }, {
20
20
  readonly pattern: RegExp;
21
- readonly normalized: "Cannot set property/properties of null";
21
+ readonly normalized: "Cannot set properties of null";
22
22
  }, {
23
23
  readonly pattern: RegExp;
24
- readonly normalized: "Cannot set property/properties of undefined";
24
+ readonly normalized: "Cannot set properties of undefined";
25
25
  }, {
26
26
  readonly pattern: RegExp;
27
27
  readonly normalized: "Cannot access property of undefined";
@@ -17,19 +17,19 @@ const commonErrors = [
17
17
  },
18
18
  {
19
19
  pattern: /cannot read (property|properties).* of null/iu,
20
- normalized: 'Cannot read property/properties of null',
20
+ normalized: 'Cannot read properties of null',
21
21
  },
22
22
  {
23
23
  pattern: /cannot read (property|properties).* of undefined/iu,
24
- normalized: 'Cannot read property/properties of undefined',
24
+ normalized: 'Cannot read properties of undefined',
25
25
  },
26
26
  {
27
27
  pattern: /cannot set (property|properties).* of null/iu,
28
- normalized: 'Cannot set property/properties of null',
28
+ normalized: 'Cannot set properties of null',
29
29
  },
30
30
  {
31
31
  pattern: /cannot set (property|properties).* of undefined/iu,
32
- normalized: 'Cannot set property/properties of undefined',
32
+ normalized: 'Cannot set properties of undefined',
33
33
  },
34
34
  {
35
35
  pattern: /undefined is not an object \(evaluating '.*'\)/iu,
package/lib/safe-error.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getAllowlist, isSafe } from '@exodus/safe-string';
1
+ import { getAllowlist, parseString } from '@exodus/safe-string';
2
2
  import assert from 'minimalistic-assert';
3
3
  import parseStackTraceNatively, { stackFramesToString } from './stack.js';
4
4
  import { omitUndefined } from './utils.js';
@@ -26,10 +26,6 @@ function isSafeCode(value) {
26
26
  return SAFE_CODES_SET.has(value) || isExodusErrorCode(value);
27
27
  }
28
28
  const staticAllowlist = getAllowlist();
29
- function getSafeString(str) {
30
- if (isSafe(str))
31
- return str;
32
- }
33
29
  const FACTORY_SYMBOL = Symbol('SafeError');
34
30
  export class SafeError {
35
31
  static from(err) {
@@ -44,7 +40,7 @@ export class SafeError {
44
40
  const hintCandidates = [hint, message].filter((str) => typeof str === 'string');
45
41
  safeHint =
46
42
  // chicken sacrifice to TypeScript, otherwise would be hintCandidates.find((str) => isSafe(str))
47
- hintCandidates.map((str) => getSafeString(str)).find(Boolean) ||
43
+ hintCandidates.map((str) => parseString(str)).find(Boolean) ||
48
44
  staticAllowlist.find((safePrefix) => hintCandidates.some((str) => str.startsWith(safePrefix))) ||
49
45
  toCommonErrorMessage(message);
50
46
  const safeStack = parseStackTraceNatively(err);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exodus/errors",
3
3
  "type": "module",
4
- "version": "3.0.0",
4
+ "version": "3.1.0",
5
5
  "description": "Utilities for error handling in client code, such as sanitization",
6
6
  "author": "Exodus Movement, Inc.",
7
7
  "repository": {
@@ -24,7 +24,7 @@
24
24
  "CHANGELOG.md"
25
25
  ],
26
26
  "scripts": {
27
- "build": "run -T tsc --build tsconfig.build.json",
27
+ "build": "run -T tsc -p tsconfig.build.json",
28
28
  "prepublishOnly": "yarn run -T build --scope @exodus/errors",
29
29
  "clean": "run -T tsc --build --clean",
30
30
  "lint": "run -T eslint .",
@@ -34,7 +34,7 @@
34
34
  "test:hermes": "EXODUS_TEST_COVERAGE=0 run -T exodus-test --engine hermes:bundle --esbuild --jest src/__tests__/hermes/*.test.ts src/__tests__/engine-agnostic/*.test.ts"
35
35
  },
36
36
  "dependencies": {
37
- "@exodus/safe-string": "^1.0.0",
37
+ "@exodus/safe-string": "^1.2.0",
38
38
  "minimalistic-assert": "^1.0.1"
39
39
  },
40
40
  "devDependencies": {
@@ -44,5 +44,5 @@
44
44
  "publishConfig": {
45
45
  "access": "public"
46
46
  },
47
- "gitHead": "aca2b6b0dbe54c8b2b14b14412c81d0237b8d5ee"
47
+ "gitHead": "917eaccefaf98df19b72a145a52510759cea1237"
48
48
  }