-
Notifications
You must be signed in to change notification settings - Fork 399
feat(shared): Introduce base ClerkError and type guard helpers #6985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
90cbe54
a4e46b4
0adebe3
ba2a3cc
0c96329
8ed495e
8662db6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| --- | ||
| '@clerk/shared': minor | ||
| --- | ||
|
|
||
| Internal refactor of error handling to improve type safety and error classification. | ||
|
|
||
| - Introduce new `ClerkError` base class for all Clerk errors | ||
| - Rename internal error files: `apiResponseError.ts` → `clerkApiResponseError.ts`, `runtimeError.ts` → `clerkRuntimeError.ts` | ||
| - Add `ClerkAPIError` class for individual API errors with improved type safety | ||
| - Add type guard utilities (`isClerkError`, `isClerkRuntimeError`, `isClerkApiResponseError`) for better error handling | ||
| - Deprecate `clerkRuntimeError` property in favor of `clerkError` for consistency | ||
| - Add support for error codes, long messages, and documentation URLs | ||
|
|
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import type { ClerkAPIError as ClerkAPIErrorInterface, ClerkAPIErrorJSON } from '@clerk/types'; | ||
|
|
||
| import { createErrorTypeGuard } from './createErrorTypeGuard'; | ||
|
|
||
| export type ClerkApiErrorMeta = Record<string, unknown>; | ||
|
|
||
| /** | ||
| * This error contains the specific error message, code, and any additional metadata that was returned by the Clerk API. | ||
| */ | ||
| export class ClerkAPIError<Meta extends ClerkApiErrorMeta = any> implements ClerkAPIErrorInterface { | ||
| readonly name = 'ClerkApiError'; | ||
| readonly code: string; | ||
| readonly message: string; | ||
| readonly longMessage: string | undefined; | ||
| readonly meta: Meta; | ||
|
|
||
| constructor(json: ClerkAPIErrorJSON) { | ||
| const parsedError = this.parseJsonError(json); | ||
| this.code = parsedError.code; | ||
| this.message = parsedError.message; | ||
| this.longMessage = parsedError.longMessage; | ||
| this.meta = parsedError.meta; | ||
| } | ||
|
|
||
| private parseJsonError(json: ClerkAPIErrorJSON) { | ||
| return { | ||
| code: json.code, | ||
| message: json.message, | ||
| longMessage: json.long_message, | ||
| meta: { | ||
| paramName: json.meta?.param_name, | ||
| sessionId: json.meta?.session_id, | ||
| emailAddresses: json.meta?.email_addresses, | ||
| identifiers: json.meta?.identifiers, | ||
| zxcvbn: json.meta?.zxcvbn, | ||
| plan: json.meta?.plan, | ||
| isPlanUpgradePossible: json.meta?.is_plan_upgrade_possible, | ||
| } as unknown as Meta, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Type guard to check if a value is a ClerkApiError instance. | ||
| */ | ||
| export const isClerkApiError = createErrorTypeGuard(ClerkAPIError); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||||||||||||||
| import type { ClerkAPIErrorJSON, ClerkAPIResponseError as ClerkAPIResponseErrorInterface } from '@clerk/types'; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { ClerkAPIError } from './clerkApiError'; | ||||||||||||||||||
| import type { ClerkErrorParams } from './clerkError'; | ||||||||||||||||||
| import { ClerkError } from './clerkError'; | ||||||||||||||||||
| import { createErrorTypeGuard } from './createErrorTypeGuard'; | ||||||||||||||||||
|
|
||||||||||||||||||
| interface ClerkAPIResponseOptions extends Omit<ClerkErrorParams, 'message' | 'code'> { | ||||||||||||||||||
| data: ClerkAPIErrorJSON[]; | ||||||||||||||||||
| status: number; | ||||||||||||||||||
| clerkTraceId?: string; | ||||||||||||||||||
| retryAfter?: number; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| export class ClerkAPIResponseError extends ClerkError implements ClerkAPIResponseErrorInterface { | ||||||||||||||||||
| static name = 'ClerkAPIResponseError'; | ||||||||||||||||||
| status: number; | ||||||||||||||||||
| clerkTraceId?: string; | ||||||||||||||||||
| retryAfter?: number; | ||||||||||||||||||
| errors: ClerkAPIError[]; | ||||||||||||||||||
|
Comment on lines
+17
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Make instance properties readonly for consistency with base class. All instance properties in the base Apply this diff: - status: number;
- clerkTraceId?: string;
- retryAfter?: number;
- errors: ClerkAPIError[];
+ readonly status: number;
+ readonly clerkTraceId?: string;
+ readonly retryAfter?: number;
+ readonly errors: ClerkAPIError[];Based on coding guidelines. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| constructor(message: string, options: ClerkAPIResponseOptions) { | ||||||||||||||||||
| const { data: errorsJson, status, clerkTraceId, retryAfter } = options; | ||||||||||||||||||
| super({ ...options, message, code: 'api_response_error' }); | ||||||||||||||||||
| Object.setPrototypeOf(this, ClerkAPIResponseError.prototype); | ||||||||||||||||||
| this.status = status; | ||||||||||||||||||
| this.clerkTraceId = clerkTraceId; | ||||||||||||||||||
| this.retryAfter = retryAfter; | ||||||||||||||||||
| this.errors = (errorsJson || []).map(e => new ClerkAPIError(e)); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| public toString() { | ||||||||||||||||||
| let message = `[${this.name}]\nMessage:${this.message}\nStatus:${this.status}\nSerialized errors: ${this.errors.map( | ||||||||||||||||||
| e => JSON.stringify(e), | ||||||||||||||||||
| )}`; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (this.clerkTraceId) { | ||||||||||||||||||
| message += `\nClerk Trace ID: ${this.clerkTraceId}`; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return message; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Override formatMessage to keep it unformatted for backward compatibility | ||||||||||||||||||
| protected static override formatMessage(name: string, msg: string, _: string, __: string | undefined) { | ||||||||||||||||||
| return msg; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Type guard to check if an error is a ClerkApiResponseError. | ||||||||||||||||||
| * Can be called as a standalone function or as a method on an error object. | ||||||||||||||||||
| * | ||||||||||||||||||
| * @example | ||||||||||||||||||
| * // As a standalone function | ||||||||||||||||||
| * if (isClerkApiResponseError(error)) { ... } | ||||||||||||||||||
| * | ||||||||||||||||||
| * // As a method (when attached to error object) | ||||||||||||||||||
| * if (error.isClerkApiResponseError()) { ... } | ||||||||||||||||||
| */ | ||||||||||||||||||
| export const isClerkApiResponseError = createErrorTypeGuard(ClerkAPIResponseError); | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,83 @@ | ||||||||||||||||||||||||||||||||||
| import { createErrorTypeGuard } from './createErrorTypeGuard'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export interface ClerkErrorParams { | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * A message that describes the error. This is typically intented to be showed to the developers. | ||||||||||||||||||||||||||||||||||
nikosdouvlis marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
| * It should not be shown to the user or parsed directly as the message contents are not guaranteed | ||||||||||||||||||||||||||||||||||
| * to be stable - use the `code` property instead. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| message: string; | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * A machine-stable code that identifies the error. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| code: string; | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * A user-friendly message that describes the error and can be displayed to the user. | ||||||||||||||||||||||||||||||||||
| * This message defaults to English but can be usually translated to the user's language | ||||||||||||||||||||||||||||||||||
| * by matching the `code` property to a localized message. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| longMessage?: string; | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * The cause of the error, typically an `Error` instance that was caught and wrapped by the Clerk error handler. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| cause?: Error; | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * A URL to the documentation for the error. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| docsUrl?: string; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * A temporary placeholder, this will eventually be replaced with a | ||||||||||||||||||||||||||||||||||
| * build-time flag that will actually perform DCE. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| const __DEV__ = true; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export class ClerkError extends Error { | ||||||||||||||||||||||||||||||||||
| static name = 'ClerkError'; | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove static name assignment; it can throw at runtime Assigning to Function.name is non-writable and may throw. The base getter returns constructor.name already. - static name = 'ClerkError';
+ // Name resolves from constructor via getter; avoid static assignment.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| readonly clerkError = true as const; | ||||||||||||||||||||||||||||||||||
| readonly code: string; | ||||||||||||||||||||||||||||||||||
| readonly longMessage: string | undefined; | ||||||||||||||||||||||||||||||||||
| readonly docsUrl: string | undefined; | ||||||||||||||||||||||||||||||||||
| readonly cause: Error | undefined; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| get name() { | ||||||||||||||||||||||||||||||||||
| return this.constructor.name; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| constructor(opts: ClerkErrorParams) { | ||||||||||||||||||||||||||||||||||
| super(new.target.formatMessage(new.target.name, opts.message, opts.code, opts.docsUrl), { cause: opts.cause }); | ||||||||||||||||||||||||||||||||||
| Object.setPrototypeOf(this, ClerkError.prototype); | ||||||||||||||||||||||||||||||||||
| this.code = opts.code; | ||||||||||||||||||||||||||||||||||
| this.docsUrl = opts.docsUrl; | ||||||||||||||||||||||||||||||||||
| this.longMessage = opts.longMessage; | ||||||||||||||||||||||||||||||||||
| this.cause = opts.cause; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| public toString() { | ||||||||||||||||||||||||||||||||||
| return `[${this.name}]\nMessage:${this.message}`; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| protected static formatMessage(name: string, msg: string, code: string, docsUrl: string | undefined) { | ||||||||||||||||||||||||||||||||||
| // Keeping the Clerk prefix for backward compatibility | ||||||||||||||||||||||||||||||||||
| // msg = `${name}: ${msg.trim()}\n\n(code="${code}")\n\n`; | ||||||||||||||||||||||||||||||||||
| // We can remove the Clerk prefix in the next major version | ||||||||||||||||||||||||||||||||||
| const prefix = 'Clerk:'; | ||||||||||||||||||||||||||||||||||
| const regex = new RegExp(prefix.replace(' ', '\\s*'), 'i'); | ||||||||||||||||||||||||||||||||||
| msg = msg.replace(regex, ''); | ||||||||||||||||||||||||||||||||||
| msg = `${prefix} ${msg.trim()}\n\n(code="${code}")\n\n`; | ||||||||||||||||||||||||||||||||||
| if (__DEV__ && docsUrl) { | ||||||||||||||||||||||||||||||||||
| msg += `\n\nDocs: ${docsUrl}`; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return msg; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Type guard to check if a value is a ClerkError instance. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export function isClerkError(val: unknown): val is ClerkError { | ||||||||||||||||||||||||||||||||||
| const typeguard = createErrorTypeGuard(ClerkError); | ||||||||||||||||||||||||||||||||||
| // Ths is the base error so we're being more defensive about the type guard | ||||||||||||||||||||||||||||||||||
| return typeguard(val) || (!!val && typeof val === 'object' && 'clerkError' in val && val.clerkError === true); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+79
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the fallback type guard type-safe Avoid property access on unknown; narrow safely. -export function isClerkError(val: unknown): val is ClerkError {
- const typeguard = createErrorTypeGuard(ClerkError);
- // Ths is the base error so we're being more defensive about the type guard
- return typeguard(val) || (!!val && typeof val === 'object' && 'clerkError' in val && val.clerkError === true);
-}
+export function isClerkError(val: unknown): val is ClerkError {
+ const typeguard = createErrorTypeGuard(ClerkError);
+ if (typeguard(val)) return true;
+ if (typeof val === 'object' && val !== null) {
+ const v = val as { clerkError?: unknown };
+ return v.clerkError === true;
+ }
+ return false;
+}Based on coding guidelines (proper type annotations; avoid unsafe property access). 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||
| import type { ClerkErrorParams } from './clerkError'; | ||||||
| import { ClerkError } from './clerkError'; | ||||||
| import { createErrorTypeGuard } from './createErrorTypeGuard'; | ||||||
|
|
||||||
| type ClerkRuntimeErrorOptions = Omit<ClerkErrorParams, 'message'>; | ||||||
|
|
||||||
| /** | ||||||
| * Custom error class for representing Clerk runtime errors. | ||||||
| * | ||||||
| * @class ClerkRuntimeError | ||||||
| * | ||||||
| * @example | ||||||
| * throw new ClerkRuntimeError('An error occurred', { code: 'password_invalid' }); | ||||||
| */ | ||||||
| export class ClerkRuntimeError extends ClerkError { | ||||||
| static name = 'ClerkRuntimeError'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove static name assignment; Function.name is non‑writable and can throw Rely on the base getter using - static name = 'ClerkRuntimeError';
+ // Name resolves from constructor via base class getter.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| /** | ||||||
| * @deprecated Use `clerkError` property instead. This property is maintained for backward compatibility. | ||||||
| */ | ||||||
| readonly clerkRuntimeError = true as const; | ||||||
|
|
||||||
| constructor(message: string, options: ClerkRuntimeErrorOptions) { | ||||||
| super({ ...options, message }); | ||||||
| Object.setPrototypeOf(this, ClerkRuntimeError.prototype); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Type guard to check if an error is a ClerkRuntimeError. | ||||||
| * Can be called as a standalone function or as a method on an error object. | ||||||
| * | ||||||
| * @example | ||||||
| * // As a standalone function | ||||||
| * if (isClerkRuntimeError(error)) { ... } | ||||||
| * | ||||||
| * // As a method (when attached to error object) | ||||||
| * if (error.isClerkRuntimeError()) { ... } | ||||||
| */ | ||||||
| export const isClerkRuntimeError = createErrorTypeGuard(ClerkRuntimeError); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainAvoid duplicate guards; ensure a single exported There’s another helper with the same name relying on a marker prop ( Run to locate duplicates and re‑exports: 🏁 Script executed: #!/bin/bash
# Find all definitions/exports named isClerkRuntimeError
rg -nP '\b(export\s+(const|function)\s+isClerkRuntimeError\b|export\s*\{[^\}]*\bisClerkRuntimeError\b[^\}]*\})' -C2
# Check aggregator exports
rg -nP 'from\s+["'\''][\.\/].*error' -C2 packages/shared/src/error.ts || true
rg -nP '\bisClerkRuntimeError\b' -C3 packages/shared/src/error.ts || trueLength of output: 1500 Eliminate duplicate isClerkRuntimeError guards 🤖 Prompt for AI Agents |
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove static name assignment; it can throw at runtime
Assigning to Function.name is non-writable in JS and can throw. Rely on the base getter returning constructor.name.
Apply:
📝 Committable suggestion
🤖 Prompt for AI Agents