Skip to content

Commit 6e90c2a

Browse files
committed
introduce global error types
1 parent 2001287 commit 6e90c2a

File tree

7 files changed

+201
-28
lines changed

7 files changed

+201
-28
lines changed

packages/shared/src/errors/apiResponseError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66

77
import { parseErrors } from './parseError';
88

9-
interface ClerkAPIResponseOptions {
9+
export interface ClerkAPIResponseOptions {
1010
data: ClerkAPIErrorJSON[];
1111
status: number;
1212
clerkTraceId?: string;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* eslint-disable jsdoc/require-jsdoc */
2+
import type { ClerkAPIErrorJSON, ClerkApiErrorResponseJSON } from '@clerk/types';
3+
4+
import { parseError } from '../parseError';
5+
import { ClerkError } from './future';
6+
7+
/**
8+
* A ClerkError subclass that represents the shell response of a Clerk API error.
9+
* This error contains an array of ClerkApiError instances, each representing a specific error that occurred.
10+
*/
11+
export class ClerkApiResponseError extends ClerkError {
12+
readonly name = 'ClerkApiResponseError';
13+
readonly retryAfter?: number;
14+
readonly errors: ClerkApiError[];
15+
16+
constructor(data: ClerkApiErrorResponseJSON) {
17+
const errorMesages = data.errors.map(e => e.message).join(', ');
18+
const message = `Api errors occurred: ${errorMesages}. Check the \`errors\` property for more details about the specific errors.`;
19+
super({ message, code: 'clerk_api_error', clerkTraceId: data.clerk_trace_id });
20+
this.errors = data.errors.map(e => new ClerkApiError(e));
21+
}
22+
}
23+
24+
/**
25+
* Type guard to check if an error is a ClerkApiResponseError.
26+
* Can be called as a standalone function or as a method on an error object.
27+
*
28+
* @example
29+
* // As a standalone function
30+
* if (isClerkApiResponseError(error)) { ... }
31+
*
32+
* // As a method (when attached to error object)
33+
* if (error.isClerkApiResponseError()) { ... }
34+
*/
35+
export function isClerkApiResponseError(error: Error): error is ClerkApiResponseError;
36+
export function isClerkApiResponseError(this: Error): this is ClerkApiResponseError;
37+
export function isClerkApiResponseError(this: Error | void, error?: Error): error is ClerkApiResponseError {
38+
const target = error ?? this;
39+
if (!target) {
40+
throw new TypeError('isClerkApiResponseError requires an error object');
41+
}
42+
return target instanceof ClerkApiResponseError;
43+
}
44+
45+
/**
46+
* This error contains the specific error message, code, and any additional metadata that was returned by the Clerk API.
47+
*/
48+
export class ClerkApiError extends ClerkError {
49+
readonly name = 'ClerkApiError';
50+
51+
constructor(json: ClerkAPIErrorJSON) {
52+
const parsedError = parseError(json);
53+
super({
54+
code: parsedError.code,
55+
message: parsedError.message,
56+
longMessage: parsedError.longMessage,
57+
});
58+
}
59+
}
60+
61+
/**
62+
* Type guard to check if a value is a ClerkApiError instance.
63+
*/
64+
export function isClerkApiError(error: Error): error is ClerkApiError {
65+
return error instanceof ClerkApiError;
66+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
export interface ClerkErrorParams {
2+
/**
3+
* A message that describes the error. This is typically intented to be showed to the developers.
4+
* It should not be shown to the user or parsed directly as the message contents are not guaranteed
5+
* to be stable - use the `code` property instead.
6+
*/
7+
message: string;
8+
/**
9+
* A machine-stable code that identifies the error.
10+
*/
11+
code: string;
12+
/**
13+
* A user-friendly message that describes the error and can be displayed to the user.
14+
* This message defaults to English but can be usually translated to the user's language
15+
* by matching the `code` property to a localized message.
16+
*/
17+
longMessage?: string;
18+
/**
19+
* A trace ID that can be used to identify the error in the Clerk API logs.
20+
*/
21+
clerkTraceId?: string;
22+
kind?: string;
23+
/**
24+
* The cause of the error, typically an `Error` instance that was caught and wrapped by the Clerk error handler.
25+
*/
26+
cause?: Error;
27+
/**
28+
* A URL to the documentation for the error.
29+
*/
30+
docsUrl?: string;
31+
}
32+
33+
/**
34+
* A temporary placeholder, this will eventually be replaced with a
35+
* build-time flag that will actually perform DCE.
36+
*/
37+
const __DEV__ = true;
38+
39+
export class ClerkError extends Error {
40+
readonly clerkError = true as const;
41+
readonly name: string = 'ClerkError';
42+
readonly code: string;
43+
readonly longMessage: string | undefined;
44+
readonly clerkTraceId: string | undefined;
45+
readonly kind: string;
46+
readonly docsUrl: string | undefined;
47+
readonly cause: Error | undefined;
48+
49+
constructor(opts: ClerkErrorParams) {
50+
const formatMessage = (msg: string, code: string, docsUrl: string | undefined) => {
51+
msg = `${this.name}: ${msg.trim()}\n\n(code="${code}")\n\n`;
52+
if (__DEV__) {
53+
msg += `\n\nDocs: ${docsUrl}`;
54+
}
55+
return msg;
56+
};
57+
58+
super(formatMessage(opts.message, opts.code, opts.docsUrl), { cause: opts.cause });
59+
Object.setPrototypeOf(this, ClerkError.prototype);
60+
61+
this.code = opts.code;
62+
this.kind = opts.kind ?? 'ClerkError';
63+
this.docsUrl = opts.docsUrl;
64+
}
65+
}
66+
67+
/**
68+
* Type guard to check if a value is a ClerkError instance.
69+
*/
70+
export function isClerkError(val: unknown): val is ClerkError {
71+
return !!val && typeof val === 'object' && 'clerkError' in val && val.clerkError === true;
72+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { isClerkApiResponseError } from './clerkApiError';
2+
import type { ClerkError } from './future';
3+
4+
/**
5+
* Creates a ClerkGlobalHookError object from a ClerkError instance.
6+
* It's a wrapper for all the different instances of Clerk errors that can
7+
* be returned when using Clerk hooks.
8+
*/
9+
export function ClerkGlobalHookError(error: ClerkError) {
10+
const predicates = {
11+
isClerkApiResponseError,
12+
} as const;
13+
14+
for (const [name, fn] of Object.entries(predicates)) {
15+
Object.assign(error, { [name]: fn });
16+
}
17+
18+
return error as ClerkError & typeof predicates;
19+
}
20+
21+
const ar = ClerkGlobalHookError({} as any);
22+
23+
console.log(ar.retryAfter);
24+
if (ar.isClerkApiResponseError()) {
25+
console.log(ar.retryAfter);
26+
}

packages/types/src/errors.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { ClientJSON } from './json';
2+
3+
export interface ClerkApiErrorResponseJSON {
4+
errors: ClerkAPIErrorJSON[];
5+
clerk_trace_id?: string;
6+
meta?: { client?: ClientJSON };
7+
}
8+
9+
export interface ClerkAPIErrorJSON {
10+
code: string;
11+
message: string;
12+
long_message?: string;
13+
clerk_trace_id?: string;
14+
meta?: {
15+
param_name?: string;
16+
session_id?: string;
17+
email_addresses?: string[];
18+
identifiers?: string[];
19+
zxcvbn?: {
20+
suggestions: {
21+
code: string;
22+
message: string;
23+
}[];
24+
};
25+
plan?: {
26+
amount_formatted: string;
27+
annual_monthly_amount_formatted: string;
28+
currency_symbol: string;
29+
id: string;
30+
name: string;
31+
};
32+
is_plan_upgrade_possible?: boolean;
33+
};
34+
}

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export * from './router';
5353
/**
5454
* TODO @revamp-hooks: Drop this in the next major release.
5555
*/
56+
export * from './errors';
5657
export * from './runtime-values';
5758
export * from './saml';
5859
export * from './samlAccount';

packages/types/src/json.ts

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
import type { CommerceSettingsJSON } from './commerceSettings';
1616
import type { DisplayConfigJSON } from './displayConfig';
1717
import type { EnterpriseProtocol, EnterpriseProvider } from './enterpriseAccount';
18+
import type { ClerkAPIErrorJSON } from './errors';
1819
import type { EmailAddressIdentifier, UsernameIdentifier } from './identifiers';
1920
import type { ActClaim } from './jwtv2';
2021
import type { OAuthProvider } from './oauth';
@@ -359,33 +360,6 @@ export interface SignUpVerificationJSON extends VerificationJSON {
359360
channel?: PhoneCodeChannel;
360361
}
361362

362-
export interface ClerkAPIErrorJSON {
363-
code: string;
364-
message: string;
365-
long_message?: string;
366-
clerk_trace_id?: string;
367-
meta?: {
368-
param_name?: string;
369-
session_id?: string;
370-
email_addresses?: string[];
371-
identifiers?: string[];
372-
zxcvbn?: {
373-
suggestions: {
374-
code: string;
375-
message: string;
376-
}[];
377-
};
378-
plan?: {
379-
amount_formatted: string;
380-
annual_monthly_amount_formatted: string;
381-
currency_symbol: string;
382-
id: string;
383-
name: string;
384-
};
385-
is_plan_upgrade_possible?: boolean;
386-
};
387-
}
388-
389363
export interface TokenJSON extends ClerkResourceJSON {
390364
object: 'token';
391365
jwt: string;

0 commit comments

Comments
 (0)