From 71cb5fe5e75596b17a2a6f265496555c722a5724 Mon Sep 17 00:00:00 2001 From: ieow <4881057+ieow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:19:34 +0800 Subject: [PATCH 1/8] chore: seedless error with causes details --- .../src/SeedlessOnboardingController.ts | 8 ++- .../src/errors.ts | 72 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index 5a91543ded0..858426038d8 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -38,7 +38,7 @@ import { SeedlessOnboardingControllerErrorMessage, Web3AuthNetwork, } from './constants'; -import { PasswordSyncError, RecoveryError } from './errors'; +import { PasswordSyncError, RecoveryError, SeedlessOnboardingError } from './errors'; import { projectLogger, createModuleLogger } from './logger'; import { SecretMetadata } from './SecretMetadata'; import type { @@ -437,8 +437,10 @@ export class SeedlessOnboardingController< return authenticationResult; } catch (error) { log('Error authenticating user', error); - throw new Error( - SeedlessOnboardingControllerErrorMessage.AuthenticationError, + throw new SeedlessOnboardingError( + SeedlessOnboardingControllerErrorMessage.AuthenticationError, { + cause: error instanceof Error ? error : new Error(error as string), + } ); } }; diff --git a/packages/seedless-onboarding-controller/src/errors.ts b/packages/seedless-onboarding-controller/src/errors.ts index 7f5c0224de8..086064dc89b 100644 --- a/packages/seedless-onboarding-controller/src/errors.ts +++ b/packages/seedless-onboarding-controller/src/errors.ts @@ -135,3 +135,75 @@ export class RecoveryError extends Error { return new RecoveryError(errorMessage, recoveryErrorData); } } + +/** + * Generic error class for SeedlessOnboardingController operations. + * + * Use this when you need to wrap an underlying error with additional context, + * or when none of the more specific error classes (PasswordSyncError, RecoveryError) apply. + * + * @example + * ```typescript + * throw new SeedlessOnboardingError( + * SeedlessOnboardingControllerErrorMessage.FailedToEncryptAndStoreSecretData, + * { details: 'Encryption failed during backup', cause: originalError } + * ); + * ``` + */ +export class SeedlessOnboardingError extends Error { + /** + * Additional context about the error beyond the message. + * Use this for human-readable details that help with debugging. + */ + public details: string | undefined; + + /** + * The underlying error that caused this error. + */ + public cause: Error | undefined; + + constructor( + message: string | SeedlessOnboardingControllerErrorMessage, + options?: { details?: string; cause?: Error }, + ) { + super(message); + this.name = 'SeedlessOnboardingControllerError'; + this.details = options?.details; + this.cause = options?.cause; + } + + /** + * Creates a SeedlessOnboardingError from an unknown error. + * + * @param error - The original error to wrap. + * @param message - The error message to use. + * @param details - Optional additional context. + * @returns A new SeedlessOnboardingError instance. + */ + static from( + error: unknown, + message: string | SeedlessOnboardingControllerErrorMessage, + details?: string, + ): SeedlessOnboardingError { + const cause = error instanceof Error ? error : undefined; + return new SeedlessOnboardingError(message, { details, cause }); + } + + /** + * Serializes the error for logging/transmission. + * Ensures custom properties are included in JSON output. + * + * @returns A JSON-serializable representation of the error. + */ + toJSON(): Record { + return { + name: this.name, + message: this.message, + details: this.details, + cause: this.cause instanceof Error + ? { name: this.cause.name, message: this.cause.message } + : this.cause, + stack: this.stack, + }; + } +} From 3f36d61c16ec127083a38b54b3aaacea4c153216 Mon Sep 17 00:00:00 2001 From: ieow <4881057+ieow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:37:40 +0800 Subject: [PATCH 2/8] fix: address comment --- packages/seedless-onboarding-controller/src/errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/seedless-onboarding-controller/src/errors.ts b/packages/seedless-onboarding-controller/src/errors.ts index 086064dc89b..f11ff464657 100644 --- a/packages/seedless-onboarding-controller/src/errors.ts +++ b/packages/seedless-onboarding-controller/src/errors.ts @@ -185,7 +185,7 @@ export class SeedlessOnboardingError extends Error { message: string | SeedlessOnboardingControllerErrorMessage, details?: string, ): SeedlessOnboardingError { - const cause = error instanceof Error ? error : undefined; + const cause = error instanceof Error ? error : new Error(String(error)); return new SeedlessOnboardingError(message, { details, cause }); } From 8d8c7bf2e0848c996fa1a2d0dac303c25e26bccf Mon Sep 17 00:00:00 2001 From: ieow <4881057+ieow@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:51:26 +0800 Subject: [PATCH 3/8] fix: address comment --- .../src/SeedlessOnboardingController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index 858426038d8..168e6b07098 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -439,7 +439,7 @@ export class SeedlessOnboardingController< log('Error authenticating user', error); throw new SeedlessOnboardingError( SeedlessOnboardingControllerErrorMessage.AuthenticationError, { - cause: error instanceof Error ? error : new Error(error as string), + cause: error instanceof Error ? error : new Error(String(error)), } ); } From e2e7213ff60625d80fb41b2134871d1b4c324fbb Mon Sep 17 00:00:00 2001 From: ieow <4881057+ieow@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:35:04 +0800 Subject: [PATCH 4/8] fix: remove from --- .../src/SeedlessOnboardingController.ts | 2 +- .../src/errors.ts | 27 ++++++------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index 168e6b07098..62ffc6efba8 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -439,7 +439,7 @@ export class SeedlessOnboardingController< log('Error authenticating user', error); throw new SeedlessOnboardingError( SeedlessOnboardingControllerErrorMessage.AuthenticationError, { - cause: error instanceof Error ? error : new Error(String(error)), + cause: error, } ); } diff --git a/packages/seedless-onboarding-controller/src/errors.ts b/packages/seedless-onboarding-controller/src/errors.ts index f11ff464657..74af8d7400e 100644 --- a/packages/seedless-onboarding-controller/src/errors.ts +++ b/packages/seedless-onboarding-controller/src/errors.ts @@ -164,29 +164,18 @@ export class SeedlessOnboardingError extends Error { constructor( message: string | SeedlessOnboardingControllerErrorMessage, - options?: { details?: string; cause?: Error }, + options?: { details?: string; cause?: unknown }, ) { super(message); this.name = 'SeedlessOnboardingControllerError'; this.details = options?.details; - this.cause = options?.cause; - } - - /** - * Creates a SeedlessOnboardingError from an unknown error. - * - * @param error - The original error to wrap. - * @param message - The error message to use. - * @param details - Optional additional context. - * @returns A new SeedlessOnboardingError instance. - */ - static from( - error: unknown, - message: string | SeedlessOnboardingControllerErrorMessage, - details?: string, - ): SeedlessOnboardingError { - const cause = error instanceof Error ? error : new Error(String(error)); - return new SeedlessOnboardingError(message, { details, cause }); + if (options?.cause) { + if (options.cause instanceof Error) { + this.cause = options.cause; + } else { + this.cause = new Error(String(options.cause)); + } + } } /** From 2c17b453acdb6a973abbd449765842ca4c32298e Mon Sep 17 00:00:00 2001 From: ieow <4881057+ieow@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:36:59 +0800 Subject: [PATCH 5/8] fix: yarn lint --- .../src/SeedlessOnboardingController.ts | 11 ++++++++--- .../seedless-onboarding-controller/src/errors.ts | 13 +++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index 62ffc6efba8..d20536b8c5e 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -38,7 +38,11 @@ import { SeedlessOnboardingControllerErrorMessage, Web3AuthNetwork, } from './constants'; -import { PasswordSyncError, RecoveryError, SeedlessOnboardingError } from './errors'; +import { + PasswordSyncError, + RecoveryError, + SeedlessOnboardingError, +} from './errors'; import { projectLogger, createModuleLogger } from './logger'; import { SecretMetadata } from './SecretMetadata'; import type { @@ -438,9 +442,10 @@ export class SeedlessOnboardingController< } catch (error) { log('Error authenticating user', error); throw new SeedlessOnboardingError( - SeedlessOnboardingControllerErrorMessage.AuthenticationError, { + SeedlessOnboardingControllerErrorMessage.AuthenticationError, + { cause: error, - } + }, ); } }; diff --git a/packages/seedless-onboarding-controller/src/errors.ts b/packages/seedless-onboarding-controller/src/errors.ts index 74af8d7400e..08befa59c94 100644 --- a/packages/seedless-onboarding-controller/src/errors.ts +++ b/packages/seedless-onboarding-controller/src/errors.ts @@ -173,7 +173,11 @@ export class SeedlessOnboardingError extends Error { if (options.cause instanceof Error) { this.cause = options.cause; } else { - this.cause = new Error(String(options.cause)); + const causeMessage = + typeof options.cause === 'string' + ? options.cause + : JSON.stringify(options.cause); + this.cause = new Error(causeMessage); } } } @@ -189,9 +193,10 @@ export class SeedlessOnboardingError extends Error { name: this.name, message: this.message, details: this.details, - cause: this.cause instanceof Error - ? { name: this.cause.name, message: this.cause.message } - : this.cause, + cause: + this.cause instanceof Error + ? { name: this.cause.name, message: this.cause.message } + : this.cause, stack: this.stack, }; } From d10aead6fd4999b304abd6f5567db1c7e5efbcc9 Mon Sep 17 00:00:00 2001 From: ieow <4881057+ieow@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:48:22 +0800 Subject: [PATCH 6/8] fix: address cursor comment --- .../seedless-onboarding-controller/src/errors.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/errors.ts b/packages/seedless-onboarding-controller/src/errors.ts index 08befa59c94..4ae14dbbbec 100644 --- a/packages/seedless-onboarding-controller/src/errors.ts +++ b/packages/seedless-onboarding-controller/src/errors.ts @@ -173,10 +173,17 @@ export class SeedlessOnboardingError extends Error { if (options.cause instanceof Error) { this.cause = options.cause; } else { - const causeMessage = - typeof options.cause === 'string' - ? options.cause - : JSON.stringify(options.cause); + let causeMessage: string; + if (typeof options.cause === 'string') { + causeMessage = options.cause; + } else { + try { + causeMessage = JSON.stringify(options.cause); + } catch { + // Handle circular references, BigInt, or other non-serializable values + causeMessage = String(options.cause); + } + } this.cause = new Error(causeMessage); } } From e6ca21f4d59d6abf1b89a77416f0a5dd1fbb6100 Mon Sep 17 00:00:00 2001 From: ieow <4881057+ieow@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:16:40 +0800 Subject: [PATCH 7/8] fix: yarn lint --- packages/seedless-onboarding-controller/src/errors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/errors.ts b/packages/seedless-onboarding-controller/src/errors.ts index 4ae14dbbbec..8a6916ad05b 100644 --- a/packages/seedless-onboarding-controller/src/errors.ts +++ b/packages/seedless-onboarding-controller/src/errors.ts @@ -180,8 +180,7 @@ export class SeedlessOnboardingError extends Error { try { causeMessage = JSON.stringify(options.cause); } catch { - // Handle circular references, BigInt, or other non-serializable values - causeMessage = String(options.cause); + causeMessage = 'Unknown error'; } } this.cause = new Error(causeMessage); From a7d521fd884855c7eaeaf82b1e75a725834c27af Mon Sep 17 00:00:00 2001 From: ieow <4881057+ieow@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:42:16 +0800 Subject: [PATCH 8/8] fix: export SeedlessOnboardingError class add testcases --- .../src/errors.test.ts | 161 +++++++++++++++++- .../src/index.ts | 2 +- 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/packages/seedless-onboarding-controller/src/errors.test.ts b/packages/seedless-onboarding-controller/src/errors.test.ts index 0011a44c87f..795280022b0 100644 --- a/packages/seedless-onboarding-controller/src/errors.test.ts +++ b/packages/seedless-onboarding-controller/src/errors.test.ts @@ -1,7 +1,10 @@ import { TOPRFErrorCode } from '@metamask/toprf-secure-backup'; import { SeedlessOnboardingControllerErrorMessage } from './constants'; -import { getErrorMessageFromTOPRFErrorCode } from './errors'; +import { + getErrorMessageFromTOPRFErrorCode, + SeedlessOnboardingError, +} from './errors'; describe('getErrorMessageFromTOPRFErrorCode', () => { it('returns TooManyLoginAttempts for RateLimitExceeded', () => { @@ -49,3 +52,159 @@ describe('getErrorMessageFromTOPRFErrorCode', () => { ).toBe('fallback'); }); }); + +describe('SeedlessOnboardingError', () => { + describe('constructor', () => { + it('creates an error with just a message', () => { + const error = new SeedlessOnboardingError('Test error message'); + + expect(error.message).toBe('Test error message'); + expect(error.name).toBe('SeedlessOnboardingControllerError'); + expect(error.details).toBeUndefined(); + expect(error.cause).toBeUndefined(); + }); + + it('creates an error with a message from SeedlessOnboardingControllerErrorMessage enum', () => { + const error = new SeedlessOnboardingError( + SeedlessOnboardingControllerErrorMessage.AuthenticationError, + ); + + expect(error.message).toBe( + SeedlessOnboardingControllerErrorMessage.AuthenticationError, + ); + expect(error.name).toBe('SeedlessOnboardingControllerError'); + }); + + it('creates an error with message and details', () => { + const error = new SeedlessOnboardingError('Test error', { + details: 'Additional context for debugging', + }); + + expect(error.message).toBe('Test error'); + expect(error.details).toBe('Additional context for debugging'); + expect(error.cause).toBeUndefined(); + }); + + it('creates an error with an Error instance as cause', () => { + const originalError = new Error('Original error'); + const error = new SeedlessOnboardingError('Wrapped error', { + cause: originalError, + }); + + expect(error.message).toBe('Wrapped error'); + expect(error.cause).toBe(originalError); + }); + + it('creates an error with a string as cause', () => { + const error = new SeedlessOnboardingError('Test error', { + cause: 'String cause message', + }); + + expect(error.cause).toBeInstanceOf(Error); + expect(error.cause?.message).toBe('String cause message'); + }); + + it('creates an error with an object as cause (JSON serializable)', () => { + const causeObject = { code: 500, reason: 'Internal error' }; + const error = new SeedlessOnboardingError('Test error', { + cause: causeObject, + }); + + expect(error.cause).toBeInstanceOf(Error); + expect(error.cause?.message).toBe(JSON.stringify(causeObject)); + }); + + it('handles circular object as cause by using fallback message', () => { + const circularObject: Record = { name: 'circular' }; + circularObject.self = circularObject; + + const error = new SeedlessOnboardingError('Test error', { + cause: circularObject, + }); + + expect(error.cause).toBeInstanceOf(Error); + expect(error.cause?.message).toBe('Unknown error'); + }); + + it('creates an error with both details and cause', () => { + const originalError = new Error('Original'); + const error = new SeedlessOnboardingError('Test error', { + details: 'Some details', + cause: originalError, + }); + + expect(error.message).toBe('Test error'); + expect(error.details).toBe('Some details'); + expect(error.cause).toBe(originalError); + }); + }); + + describe('toJSON', () => { + it('serializes error with all properties', () => { + const originalError = new Error('Original error'); + const error = new SeedlessOnboardingError('Test error', { + details: 'Debug info', + cause: originalError, + }); + + const json = error.toJSON(); + + expect(json.name).toBe('SeedlessOnboardingControllerError'); + expect(json.message).toBe('Test error'); + expect(json.details).toBe('Debug info'); + expect(json.cause).toStrictEqual({ + name: 'Error', + message: 'Original error', + }); + expect(json.stack).toBeDefined(); + }); + + it('serializes error without optional properties', () => { + const error = new SeedlessOnboardingError('Simple error'); + + const json = error.toJSON(); + + expect(json.name).toBe('SeedlessOnboardingControllerError'); + expect(json.message).toBe('Simple error'); + expect(json.details).toBeUndefined(); + expect(json.cause).toBeUndefined(); + expect(json.stack).toBeDefined(); + }); + + it('serializes error with custom error type as cause', () => { + class CustomError extends Error { + constructor() { + super('Custom error message'); + this.name = 'CustomError'; + } + } + const customError = new CustomError(); + const error = new SeedlessOnboardingError('Wrapper', { + cause: customError, + }); + + const json = error.toJSON(); + + expect(json.cause).toStrictEqual({ + name: 'CustomError', + message: 'Custom error message', + }); + }); + }); + + describe('inheritance', () => { + it('is an instance of Error', () => { + const error = new SeedlessOnboardingError('Test'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SeedlessOnboardingError); + }); + + it('has a proper stack trace', () => { + const error = new SeedlessOnboardingError('Test'); + + expect(error.stack).toBeDefined(); + expect(error.stack).toContain('SeedlessOnboardingError'); + }); + }); +}); diff --git a/packages/seedless-onboarding-controller/src/index.ts b/packages/seedless-onboarding-controller/src/index.ts index 4d445795530..1cdde0d2a84 100644 --- a/packages/seedless-onboarding-controller/src/index.ts +++ b/packages/seedless-onboarding-controller/src/index.ts @@ -22,4 +22,4 @@ export { SecretType, } from './constants'; export { SecretMetadata } from './SecretMetadata'; -export { RecoveryError } from './errors'; +export { RecoveryError, SeedlessOnboardingError } from './errors';