From eb88cb263a97a3db2e1070e82058e3876dbd5292 Mon Sep 17 00:00:00 2001 From: Binary Date: Tue, 29 Sep 2026 12:15:35 +0300 Subject: [PATCH] fix(utils): make hasErrorCode safe for nullish catch bindings --- src/utils/utils.ts | 16 ++++++++++++++-- test/sdk/misc.spec.ts | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 9268d224a..2a7da564e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -9,7 +9,19 @@ interface ErrorWithCode extends Error { code: string } +/** + * Narrow an `unknown` catch binding to an error carrying a `code`. + * + * `null` and `undefined` are legal `unknown` values — `throw null` and + * `Promise.reject()` with no reason both produce one — so they have to be ruled + * out before the property is read. Reading `.code` off a nullish value throws + * `TypeError: Cannot read properties of null`, which is the opposite of what a + * guard is for: the caller loses the error it was about to inspect or rethrow + * and gets one that says nothing about what actually failed. + */ export const hasErrorCode = (error: unknown): error is ErrorWithCode => { - const untypedError = error as Partial - return !!untypedError.code + if (error === null || error === undefined) { + return false + } + return !!(error as Partial).code } diff --git a/test/sdk/misc.spec.ts b/test/sdk/misc.spec.ts index b11efc024..781278bbb 100644 --- a/test/sdk/misc.spec.ts +++ b/test/sdk/misc.spec.ts @@ -6,6 +6,7 @@ import { } from "../../src/constants" import { decodeTokenIds, + hasErrorCode, remapSharedStorefrontAddress, } from "../../src/utils/utils" import { BAYC_CONTRACT_ADDRESS } from "../utils/constants" @@ -172,4 +173,21 @@ describe("SDK: misc", () => { ]) }) }) + + describe("hasErrorCode", () => { + it("should return true for an error carrying a code", () => { + expect(hasErrorCode({ code: "CALL_EXCEPTION" })).toBe(true) + expect(hasErrorCode(new Error("boom"))).toBe(false) + }) + + it("should return false for nullish input instead of throwing", () => { + // `null` and `undefined` are legal `unknown` values and both reach a + // `catch` binding (`throw null`, `Promise.reject()`). The guard read + // `.code` straight off the argument, so it threw a TypeError and hid the + // error its caller was about to rethrow. + expect(() => hasErrorCode(null)).not.toThrow() + expect(hasErrorCode(null)).toBe(false) + expect(hasErrorCode(undefined)).toBe(false) + }) + }) })