Skip to content

Commit 164f3aa

Browse files
authored
feat(clerk-js,shared,types): Add ClerkRuntimeError class (#1813)
With the introduction of this class, we can localize error messages and display them in ClerkJS components. This functionality existed for FAPI errors, and we are now adding support for runtime errors.
1 parent ccf4210 commit 164f3aa

File tree

10 files changed

+126
-15
lines changed

10 files changed

+126
-15
lines changed

.changeset/curly-scissors-buy.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@clerk/localizations': patch
3+
'@clerk/clerk-js': patch
4+
'@clerk/shared': patch
5+
'@clerk/types': patch
6+
---
7+
8+
Introduce ClerkRuntimeError class for localizing error messages in ClerkJS components

packages/clerk-js/src/core/resources/Error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ export {
88
parseErrors,
99
MagicLinkError,
1010
ClerkAPIResponseError,
11+
isClerkRuntimeError,
1112
} from '@clerk/shared';
1213
export type { MetamaskError } from '@clerk/shared';

packages/clerk-js/src/core/resources/SignUp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Poller } from '@clerk/shared';
1+
import { ClerkRuntimeError, Poller } from '@clerk/shared';
22
import type {
33
AttemptEmailAddressVerificationParams,
44
AttemptPhoneNumberVerificationParams,
@@ -80,7 +80,7 @@ export class SignUp extends BaseResource implements SignUpResource {
8080
if (e.captchaError) {
8181
paramsWithCaptcha.captchaError = e.captchaError;
8282
} else {
83-
throw e;
83+
throw new ClerkRuntimeError(e.message, { code: 'captcha_unavailable' });
8484
}
8585
}
8686
}

packages/clerk-js/src/ui/elements/contexts/CardStateContext.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContextAndHook } from '@clerk/shared';
2-
import type { ClerkAPIError } from '@clerk/types';
2+
import type { ClerkAPIError, ClerkRuntimeError } from '@clerk/types';
33
import React from 'react';
44

55
import { useLocalizations } from '../../customizables';
@@ -31,7 +31,8 @@ const useCardState = () => {
3131
const { translateError } = useLocalizations();
3232

3333
const setIdle = (metadata?: Metadata) => setState(s => ({ ...s, status: 'idle', metadata }));
34-
const setError = (metadata: ClerkAPIError | Metadata) => setState(s => ({ ...s, error: translateError(metadata) }));
34+
const setError = (metadata: ClerkRuntimeError | ClerkAPIError | Metadata) =>
35+
setState(s => ({ ...s, error: translateError(metadata) }));
3536
const setLoading = (metadata?: Metadata) => setState(s => ({ ...s, status: 'loading', metadata }));
3637
const runAsync = async <T = unknown,>(cb: Promise<T> | (() => Promise<T>), metadata?: Metadata) => {
3738
setLoading(metadata);

packages/clerk-js/src/ui/localization/makeLocalizable.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { ClerkAPIError, LocalizationResource } from '@clerk/types';
1+
import { isClerkRuntimeError } from '@clerk/shared';
2+
import type { ClerkAPIError, ClerkRuntimeError, LocalizationResource } from '@clerk/types';
23
import React from 'react';
34

45
import { useOptions } from '../contexts';
@@ -70,11 +71,16 @@ export const useLocalizations = () => {
7071
return localizedStringFromKey(localizationKey, parsedResource, globalTokens);
7172
};
7273

73-
const translateError = (error: ClerkAPIError | string | undefined) => {
74+
const translateError = (error: ClerkRuntimeError | ClerkAPIError | string | undefined) => {
7475
if (!error || typeof error === 'string') {
7576
return t(error);
7677
}
77-
const { code, message, longMessage, meta } = error || {};
78+
79+
if (isClerkRuntimeError(error)) {
80+
return t(localizationKeys(`unstable__errors.${error.code}` as any)) || error.message;
81+
}
82+
83+
const { code, message, longMessage, meta } = (error || {}) as ClerkAPIError;
7884
const { paramName = '' } = meta || {};
7985

8086
if (!code) {

packages/clerk-js/src/ui/utils/errorHandler.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { snakeToCamel } from '@clerk/shared';
2-
import type { ClerkAPIError } from '@clerk/types';
3-
4-
import { isClerkAPIResponseError, isKnownError, isMetamaskError } from '../../core/resources/internal';
2+
import type { ClerkAPIError, ClerkRuntimeError } from '@clerk/types';
3+
4+
import {
5+
isClerkAPIResponseError,
6+
isClerkRuntimeError,
7+
isKnownError,
8+
isMetamaskError,
9+
} from '../../core/resources/internal';
510
import type { FormControlState } from './useFormControl';
611

712
interface ParserErrors {
@@ -52,7 +57,7 @@ type HandleError = {
5257
(
5358
err: Error,
5459
fieldStates: Array<FormControlState<string>>,
55-
setGlobalError?: (err: ClerkAPIError | string | undefined) => void,
60+
setGlobalError?: (err: ClerkRuntimeError | ClerkAPIError | string | undefined) => void,
5661
): void;
5762
};
5863

@@ -69,6 +74,10 @@ export const handleError: HandleError = (err, fieldStates, setGlobalError) => {
6974
if (isClerkAPIResponseError(err)) {
7075
return handleClerkApiError(err, fieldStates, setGlobalError);
7176
}
77+
78+
if (isClerkRuntimeError(err)) {
79+
return handleClerkRuntimeError(err, fieldStates, setGlobalError);
80+
}
7281
};
7382

7483
// Returns the first global API error or undefined if none exists.
@@ -123,3 +132,17 @@ const handleClerkApiError: HandleError = (err, fieldStates, setGlobalError) => {
123132
}
124133
}
125134
};
135+
136+
const handleClerkRuntimeError: HandleError = (err, _, setGlobalError) => {
137+
if (!isClerkRuntimeError(err)) {
138+
return;
139+
}
140+
141+
if (setGlobalError) {
142+
setGlobalError(undefined);
143+
const firstGlobalError = err;
144+
if (firstGlobalError) {
145+
setGlobalError(firstGlobalError);
146+
}
147+
}
148+
};

packages/localizations/src/en-US.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,8 @@ export const enUS: LocalizationResource = {
716716
identification_deletion_failed: 'You cannot delete your last identification.',
717717
phone_number_exists: 'This phone number is taken. Please try another.',
718718
form_identifier_not_found: '',
719+
captcha_unavailable:
720+
'Sign up unsuccessful due to failed bot validation. Please refresh the page to try again or reach out to support for more assistance.',
719721
captcha_invalid:
720722
'Sign up unsuccessful due to failed security validations. Please refresh the page to try again or reach out to support for more assistance.',
721723
form_password_pwned:

packages/shared/src/errors/Error.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,37 @@ export interface MetamaskError extends Error {
1414
}
1515

1616
export function isKnownError(error: any) {
17-
return isClerkAPIResponseError(error) || isMetamaskError(error);
17+
return isClerkAPIResponseError(error) || isMetamaskError(error) || isClerkRuntimeError(error);
1818
}
1919

2020
export function isClerkAPIResponseError(err: any): err is ClerkAPIResponseError {
21+
if (err instanceof ClerkAPIResponseError) {
22+
return true;
23+
}
24+
2125
return 'clerkError' in err;
2226
}
2327

28+
/**
29+
* Checks if the provided error object is an instance of ClerkRuntimeError.
30+
*
31+
* @param {any} err - The error object to check.
32+
* @returns {boolean} True if the error is a ClerkRuntimeError, false otherwise.
33+
*
34+
* @example
35+
* const error = new ClerkRuntimeError('An error occurred');
36+
* if (isClerkRuntimeError(error)) {
37+
* // Handle ClerkRuntimeError
38+
* console.error('ClerkRuntimeError:', error.message);
39+
* } else {
40+
* // Handle other errors
41+
* console.error('Other error:', error.message);
42+
* }
43+
*/
44+
export function isClerkRuntimeError(err: any): err is ClerkRuntimeError {
45+
return err instanceof ClerkRuntimeError;
46+
}
47+
2448
export function isMetamaskError(err: any): err is MetamaskError {
2549
return 'code' in err && [4001, 32602, 32603].includes(err.code) && 'message' in err;
2650
}
@@ -44,8 +68,6 @@ export function parseError(error: ClerkAPIErrorJSON): ClerkAPIError {
4468
}
4569

4670
export class ClerkAPIResponseError extends Error {
47-
clerkError: true;
48-
4971
status: number;
5072
message: string;
5173

@@ -58,7 +80,6 @@ export class ClerkAPIResponseError extends Error {
5880

5981
this.status = status;
6082
this.message = message;
61-
this.clerkError = true;
6283
this.errors = parseErrors(data);
6384
}
6485

@@ -69,6 +90,49 @@ export class ClerkAPIResponseError extends Error {
6990
};
7091
}
7192

93+
/**
94+
* Custom error class for representing Clerk runtime errors.
95+
*
96+
* @class ClerkRuntimeError
97+
* @example
98+
* throw new ClerkRuntimeError('An error occurred', { code: 'password_invalid' });
99+
*/
100+
export class ClerkRuntimeError extends Error {
101+
/**
102+
* The error message.
103+
*
104+
* @type {string}
105+
* @memberof ClerkRuntimeError
106+
*/
107+
message: string;
108+
109+
/**
110+
* A unique code identifying the error, used for localization
111+
*
112+
* @type {string}
113+
* @memberof ClerkRuntimeError
114+
*/
115+
code: string;
116+
constructor(message: string, { code }: { code: string }) {
117+
super(message);
118+
119+
Object.setPrototypeOf(this, ClerkRuntimeError.prototype);
120+
121+
this.code = code;
122+
this.message = message;
123+
}
124+
125+
/**
126+
* Returns a string representation of the error.
127+
*
128+
* @returns {string} A formatted string with the error name and message.
129+
* @memberof ClerkRuntimeError
130+
*/
131+
public toString = () => {
132+
return `[${this.name}]\nMessage:${this.message}`;
133+
};
134+
}
135+
72136
export class MagicLinkError extends Error {
73137
code: string;
74138

packages/types/src/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export interface ClerkAPIError {
1818
};
1919
}
2020

21+
export interface ClerkRuntimeError {
22+
code: string;
23+
message: string;
24+
}
25+
2126
/**
2227
* Pagination params
2328
*/

packages/types/src/localization.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,7 @@ type UnstableErrors = WithParamName<{
749749
identification_deletion_failed: LocalizationValue;
750750
phone_number_exists: LocalizationValue;
751751
form_identifier_not_found: LocalizationValue;
752+
captcha_unavailable: LocalizationValue;
752753
captcha_invalid: LocalizationValue;
753754
form_password_pwned: LocalizationValue;
754755
form_username_invalid_length: LocalizationValue;

0 commit comments

Comments
 (0)