Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/shared/src/errors/apiResponseError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {

import { parseErrors } from './parseError';

interface ClerkAPIResponseOptions {
export interface ClerkAPIResponseOptions {
data: ClerkAPIErrorJSON[];
status: number;
clerkTraceId?: string;
Expand Down
66 changes: 66 additions & 0 deletions packages/shared/src/errors/future/clerkApiError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable jsdoc/require-jsdoc */
import type { ClerkAPIErrorJSON, ClerkApiErrorResponseJSON } from '@clerk/types';

import { parseError } from '../parseError';
import { ClerkError } from './future';

/**
* A ClerkError subclass that represents the shell response of a Clerk API error.
* This error contains an array of ClerkApiError instances, each representing a specific error that occurred.
*/
export class ClerkApiResponseError extends ClerkError {
readonly name = 'ClerkApiResponseError';
readonly retryAfter?: number;
readonly errors: ClerkApiError[];

constructor(data: ClerkApiErrorResponseJSON) {
const errorMesages = data.errors.map(e => e.message).join(', ');
const message = `Api errors occurred: ${errorMesages}. Check the \`errors\` property for more details about the specific errors.`;
super({ message, code: 'clerk_api_error', clerkTraceId: data.clerk_trace_id });
this.errors = data.errors.map(e => new ClerkApiError(e));
}
}

/**
* 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 function isClerkApiResponseError(error: Error): error is ClerkApiResponseError;
export function isClerkApiResponseError(this: Error): this is ClerkApiResponseError;
export function isClerkApiResponseError(this: Error | void, error?: Error): error is ClerkApiResponseError {
const target = error ?? this;
if (!target) {
throw new TypeError('isClerkApiResponseError requires an error object');
}
return target instanceof ClerkApiResponseError;
}

/**
* This error contains the specific error message, code, and any additional metadata that was returned by the Clerk API.
*/
export class ClerkApiError extends ClerkError {
readonly name = 'ClerkApiError';

constructor(json: ClerkAPIErrorJSON) {
const parsedError = parseError(json);
super({
code: parsedError.code,
message: parsedError.message,
longMessage: parsedError.longMessage,
});
}
}

/**
* Type guard to check if a value is a ClerkApiError instance.
*/
export function isClerkApiError(error: Error): error is ClerkApiError {
return error instanceof ClerkApiError;
}
72 changes: 72 additions & 0 deletions packages/shared/src/errors/future/future.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export interface ClerkErrorParams {
/**
* A message that describes the error. This is typically intented to be showed to the developers.
* 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;
/**
* A trace ID that can be used to identify the error in the Clerk API logs.
*/
clerkTraceId?: string;
kind?: 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 {
readonly clerkError = true as const;
readonly name: string = 'ClerkError';
readonly code: string;
readonly longMessage: string | undefined;
readonly clerkTraceId: string | undefined;
readonly kind: string;
readonly docsUrl: string | undefined;
readonly cause: Error | undefined;

constructor(opts: ClerkErrorParams) {
const formatMessage = (msg: string, code: string, docsUrl: string | undefined) => {
msg = `${this.name}: ${msg.trim()}\n\n(code="${code}")\n\n`;
if (__DEV__) {
msg += `\n\nDocs: ${docsUrl}`;
}
return msg;
};

super(formatMessage(opts.message, opts.code, opts.docsUrl), { cause: opts.cause });
Object.setPrototypeOf(this, ClerkError.prototype);

this.code = opts.code;
this.kind = opts.kind ?? 'ClerkError';
this.docsUrl = opts.docsUrl;
}
}

/**
* Type guard to check if a value is a ClerkError instance.
*/
export function isClerkError(val: unknown): val is ClerkError {
return !!val && typeof val === 'object' && 'clerkError' in val && val.clerkError === true;
}
26 changes: 26 additions & 0 deletions packages/shared/src/errors/future/globalHookError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isClerkApiResponseError } from './clerkApiError';
import type { ClerkError } from './future';

/**
* Creates a ClerkGlobalHookError object from a ClerkError instance.
* It's a wrapper for all the different instances of Clerk errors that can
* be returned when using Clerk hooks.
*/
export function ClerkGlobalHookError(error: ClerkError) {
const predicates = {
isClerkApiResponseError,
} as const;

for (const [name, fn] of Object.entries(predicates)) {
Object.assign(error, { [name]: fn });
}

return error as ClerkError & typeof predicates;
}

const ar = ClerkGlobalHookError({} as any);

console.log(ar.retryAfter);
if (ar.isClerkApiResponseError()) {
console.log(ar.retryAfter);
}
1 change: 1 addition & 0 deletions packages/shared/src/errors/parseError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function parseError(error: ClerkAPIErrorJSON): ClerkAPIError {
code: error.code,
message: error.message,
longMessage: error.long_message,
clerkTraceId: error.clerk_trace_id,
meta: {
paramName: error?.meta?.param_name,
sessionId: error?.meta?.session_id,
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface ClerkAPIError {
* A more detailed message that describes the error.
*/
longMessage?: string;
/**
* A trace ID that can be used to identify the error in the Clerk API logs.
*/
clerkTraceId?: string;
/**
* Additional information about the error.
*/
Expand Down
34 changes: 34 additions & 0 deletions packages/types/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ClientJSON } from './json';

export interface ClerkApiErrorResponseJSON {
errors: ClerkAPIErrorJSON[];
clerk_trace_id?: string;
meta?: { client?: ClientJSON };
}

export interface ClerkAPIErrorJSON {
code: string;
message: string;
long_message?: string;
clerk_trace_id?: string;
meta?: {
param_name?: string;
session_id?: string;
email_addresses?: string[];
identifiers?: string[];
zxcvbn?: {
suggestions: {
code: string;
message: string;
}[];
};
plan?: {
amount_formatted: string;
annual_monthly_amount_formatted: string;
currency_symbol: string;
id: string;
name: string;
};
is_plan_upgrade_possible?: boolean;
};
}
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export * from './router';
/**
* TODO @revamp-hooks: Drop this in the next major release.
*/
export * from './errors';
export * from './runtime-values';
export * from './saml';
export * from './samlAccount';
Expand Down
27 changes: 1 addition & 26 deletions packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
import type { CommerceSettingsJSON } from './commerceSettings';
import type { DisplayConfigJSON } from './displayConfig';
import type { EnterpriseProtocol, EnterpriseProvider } from './enterpriseAccount';
import type { ClerkAPIErrorJSON } from './errors';
import type { EmailAddressIdentifier, UsernameIdentifier } from './identifiers';
import type { ActClaim } from './jwtv2';
import type { OAuthProvider } from './oauth';
Expand Down Expand Up @@ -359,32 +360,6 @@ export interface SignUpVerificationJSON extends VerificationJSON {
channel?: PhoneCodeChannel;
}

export interface ClerkAPIErrorJSON {
code: string;
message: string;
long_message?: string;
meta?: {
param_name?: string;
session_id?: string;
email_addresses?: string[];
identifiers?: string[];
zxcvbn?: {
suggestions: {
code: string;
message: string;
}[];
};
plan?: {
amount_formatted: string;
annual_monthly_amount_formatted: string;
currency_symbol: string;
id: string;
name: string;
};
is_plan_upgrade_possible?: boolean;
};
}

export interface TokenJSON extends ClerkResourceJSON {
object: 'token';
jwt: string;
Expand Down