@exodus/errors 3.0.1 → 3.2.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,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
+ ## [3.2.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.1.0...@exodus/errors@3.2.0) (2025-08-07)
7
+
8
+ ### Features
9
+
10
+ - feat: add more specific JSON parse error handling (#13295)
11
+
12
+ ## [3.1.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.0.1...@exodus/errors@3.1.0) (2025-08-04)
13
+
14
+ ### Features
15
+
16
+ - feat: support interpolating non-dynamic safeString into safeString on parse (#13003)
17
+
6
18
  ## [3.0.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.0.0...@exodus/errors@3.0.1) (2025-07-08)
7
19
 
8
20
  **Note:** Version bump only for package @exodus/errors
package/README.md CHANGED
@@ -45,6 +45,43 @@ Parsing/sanitization of error messages is unreliable and the cost of failure is
45
45
 
46
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
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}`)
82
+ }
83
+ ```
84
+
48
85
  ## Troubleshooting
49
86
 
50
87
  ### A SafeError instance returns `undefined` stack trace?
@@ -28,12 +28,21 @@ declare const commonErrors: readonly [{
28
28
  }, {
29
29
  readonly pattern: RegExp;
30
30
  readonly normalized: "Cannot access property of null";
31
+ }, {
32
+ readonly pattern: RegExp;
33
+ readonly normalized: "JSON Parse error: Unexpected token";
34
+ }, {
35
+ readonly pattern: RegExp;
36
+ readonly normalized: "JSON Parse error: Unexpected token";
31
37
  }, {
32
38
  readonly pattern: RegExp;
33
39
  readonly normalized: "Unexpected token";
34
40
  }, {
35
41
  readonly pattern: RegExp;
36
- readonly normalized: "Unexpected end of input";
42
+ readonly normalized: "JSON Parse error: Unexpected end of input";
43
+ }, {
44
+ readonly pattern: RegExp;
45
+ readonly normalized: "JSON Parse error";
37
46
  }];
38
47
  export type CommonErrorString = (typeof commonErrors)[number]['normalized'];
39
48
  export declare function toCommonErrorMessage(errorMessage: string): CommonErrorString | undefined;
@@ -39,13 +39,25 @@ const commonErrors = [
39
39
  pattern: /null is not an object \(evaluating '.*'\)/iu,
40
40
  normalized: 'Cannot access property of null',
41
41
  },
42
+ {
43
+ pattern: /JSON Parse error: Unexpected (token|character)/iu,
44
+ normalized: 'JSON Parse error: Unexpected token',
45
+ },
46
+ {
47
+ pattern: /Unexpected token.*is not valid JSON/iu,
48
+ normalized: 'JSON Parse error: Unexpected token',
49
+ },
42
50
  {
43
51
  pattern: /unexpected (token|character)/iu,
44
52
  normalized: 'Unexpected token',
45
53
  },
46
54
  {
47
55
  pattern: /unexpected end of( JSON)? input/iu,
48
- normalized: 'Unexpected end of input',
56
+ normalized: 'JSON Parse error: Unexpected end of input',
57
+ },
58
+ {
59
+ pattern: /^JSON Parse error:/iu,
60
+ normalized: 'JSON Parse error',
49
61
  },
50
62
  ];
51
63
  export function toCommonErrorMessage(errorMessage) {
@@ -1,7 +1,7 @@
1
1
  import type { Frame } from './types.js';
2
2
  import type { CommonErrorString } from './common-errors.js';
3
3
  type ReadonlySetValues<S> = S extends ReadonlySet<infer T> ? T : never;
4
- declare const SAFE_NAMES_SET: ReadonlySet<"Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError">;
4
+ declare const SAFE_NAMES_SET: ReadonlySet<"Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError" | "SyntaxError">;
5
5
  type StaticAllowlistString = string & {
6
6
  __branded_type: 'StaticAllowlistString';
7
7
  };
@@ -21,7 +21,7 @@ declare const FACTORY_SYMBOL: unique symbol;
21
21
  export declare class SafeError {
22
22
  #private;
23
23
  static from<T extends UnknownError>(err: T): SafeError;
24
- get name(): "Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError";
24
+ get name(): "Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError" | "SyntaxError";
25
25
  get code(): SafeCode | undefined;
26
26
  get hint(): SafeString | undefined;
27
27
  get stackFrames(): {
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';
@@ -15,6 +15,7 @@ const SAFE_NAMES_SET = makeReadonlySet([
15
15
  'UnknownError',
16
16
  'SafeErrorFailedToParse',
17
17
  'TimeoutError',
18
+ 'SyntaxError',
18
19
  ]);
19
20
  function isSafeName(value) {
20
21
  return SAFE_NAMES_SET.has(value);
@@ -26,10 +27,6 @@ function isSafeCode(value) {
26
27
  return SAFE_CODES_SET.has(value) || isExodusErrorCode(value);
27
28
  }
28
29
  const staticAllowlist = getAllowlist();
29
- function getSafeString(str) {
30
- if (isSafe(str))
31
- return str;
32
- }
33
30
  const FACTORY_SYMBOL = Symbol('SafeError');
34
31
  export class SafeError {
35
32
  static from(err) {
@@ -44,7 +41,7 @@ export class SafeError {
44
41
  const hintCandidates = [hint, message].filter((str) => typeof str === 'string');
45
42
  safeHint =
46
43
  // chicken sacrifice to TypeScript, otherwise would be hintCandidates.find((str) => isSafe(str))
47
- hintCandidates.map((str) => getSafeString(str)).find(Boolean) ||
44
+ hintCandidates.map((str) => parseString(str)).find(Boolean) ||
48
45
  staticAllowlist.find((safePrefix) => hintCandidates.some((str) => str.startsWith(safePrefix))) ||
49
46
  toCommonErrorMessage(message);
50
47
  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.1",
4
+ "version": "3.2.0",
5
5
  "description": "Utilities for error handling in client code, such as sanitization",
6
6
  "author": "Exodus Movement, Inc.",
7
7
  "repository": {
@@ -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": "7fd991f460540c1396fc68391393ee690269fa7f"
47
+ "gitHead": "b229da233485c8301fa03c8b189a55f866eb4fe0"
48
48
  }