From 784c38b88ddd7e5774c1f6c95ccdb87911ca4de5 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Fri, 7 Nov 2025 10:53:42 +0200 Subject: [PATCH 1/4] feat(react): Type `errors.global` as `ClerkGlobalHookError` --- packages/shared/src/errors/globalHookError.ts | 23 +++++++++++++++++++ packages/shared/src/types/state.ts | 6 ++++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 packages/shared/src/errors/globalHookError.ts diff --git a/packages/shared/src/errors/globalHookError.ts b/packages/shared/src/errors/globalHookError.ts new file mode 100644 index 00000000000..c4615e16234 --- /dev/null +++ b/packages/shared/src/errors/globalHookError.ts @@ -0,0 +1,23 @@ +import { isClerkApiResponseError } from './clerkApiResponseError'; +import type { ClerkError } from './clerkError'; +import { isClerkRuntimeError } from './clerkRuntimeError'; + +/** + * 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 createClerkGlobalHookError(error: ClerkError) { + const predicates = { + isClerkApiResponseError, + isClerkRuntimeError, + } as const; + + for (const [name, fn] of Object.entries(predicates)) { + Object.assign(error, { [name]: fn }); + } + + return error as ClerkError & typeof predicates; +} + +export type ClerkGlobalHookError = ReturnType; diff --git a/packages/shared/src/types/state.ts b/packages/shared/src/types/state.ts index 4438d92fe57..6fe794cbd1e 100644 --- a/packages/shared/src/types/state.ts +++ b/packages/shared/src/types/state.ts @@ -1,3 +1,4 @@ +import type { ClerkGlobalHookError } from '../errors/globalHookError'; import type { SignInFutureResource } from './signInFuture'; import type { SignUpFutureResource } from './signUpFuture'; @@ -79,8 +80,9 @@ export interface Errors { raw: unknown[] | null; /** * Parsed errors that are not related to any specific field. + * Does not include any errors that could be parsed as a field error */ - global: unknown[] | null; // does not include any errors that could be parsed as a field error + global: ClerkGlobalHookError[] | null; } /** @@ -143,6 +145,7 @@ export interface State { * An alias for `effect()` from `alien-signals`, which can be used to subscribe to changes from Signals. * * @see https://github.com/stackblitz/alien-signals#usage + * * @experimental This experimental API is subject to change. */ __internal_effect: (callback: () => void) => () => void; @@ -152,6 +155,7 @@ export interface State { * its dependencies change. * * @see https://github.com/stackblitz/alien-signals#usage + * * @experimental This experimental API is subject to change. */ __internal_computed: (getter: (previousValue?: T) => T) => () => T; From 25126a6a3fa658db8db2ffa6800cca10bf2ca49d Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:00:36 -0600 Subject: [PATCH 2/4] fix(clerk-js,shared): Replace unknown with ClerkError (#7177) Co-authored-by: Nikos Douvlis --- packages/clerk-js/src/core/events.ts | 3 +- packages/clerk-js/src/core/signals.ts | 51 ++++++++++--------- packages/clerk-js/src/core/state.ts | 3 +- packages/shared/src/error.ts | 3 ++ packages/shared/src/errors/clerkApiError.ts | 14 ++--- packages/shared/src/errors/globalHookError.ts | 2 +- 6 files changed, 40 insertions(+), 36 deletions(-) diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts index a4071653504..da4a28fdad7 100644 --- a/packages/clerk-js/src/core/events.ts +++ b/packages/clerk-js/src/core/events.ts @@ -1,3 +1,4 @@ +import type { ClerkError } from '@clerk/shared/error'; import { createEventBus } from '@clerk/shared/eventBus'; import type { TokenResource } from '@clerk/shared/types'; @@ -15,7 +16,7 @@ export const events = { type TokenUpdatePayload = { token: TokenResource | null }; export type ResourceUpdatePayload = { resource: BaseResource }; -export type ResourceErrorPayload = { resource: BaseResource; error: unknown }; +export type ResourceErrorPayload = { resource: BaseResource; error: ClerkError | null }; export type ResourceFetchPayload = { resource: BaseResource; status: 'idle' | 'fetching' }; type InternalEvents = { diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index 1228e105fe2..f8b238d3aa4 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -1,4 +1,4 @@ -import { isClerkAPIResponseError } from '@clerk/shared/error'; +import { type ClerkError, createClerkGlobalHookError, isClerkAPIResponseError } from '@clerk/shared/error'; import type { Errors, SignInSignal, SignUpSignal } from '@clerk/shared/types'; import { snakeToCamel } from '@clerk/shared/underscore'; import { computed, signal } from 'alien-signals'; @@ -7,7 +7,7 @@ import type { SignIn } from './resources/SignIn'; import type { SignUp } from './resources/SignUp'; export const signInResourceSignal = signal<{ resource: SignIn | null }>({ resource: null }); -export const signInErrorSignal = signal<{ error: unknown }>({ error: null }); +export const signInErrorSignal = signal<{ error: ClerkError | null }>({ error: null }); export const signInFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); export const signInComputedSignal: SignInSignal = computed(() => { @@ -21,7 +21,7 @@ export const signInComputedSignal: SignInSignal = computed(() => { }); export const signUpResourceSignal = signal<{ resource: SignUp | null }>({ resource: null }); -export const signUpErrorSignal = signal<{ error: unknown }>({ error: null }); +export const signUpErrorSignal = signal<{ error: ClerkError | null }>({ error: null }); export const signUpFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); export const signUpComputedSignal: SignUpSignal = computed(() => { @@ -38,7 +38,7 @@ export const signUpComputedSignal: SignUpSignal = computed(() => { * Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put * generic non-API errors into the global array. */ -function errorsToParsedErrors(error: unknown): Errors { +function errorsToParsedErrors(error: ClerkError | null): Errors { const parsedErrors: Errors = { fields: { firstName: null, @@ -62,29 +62,32 @@ function errorsToParsedErrors(error: unknown): Errors { if (!isClerkAPIResponseError(error)) { parsedErrors.raw = [error]; - parsedErrors.global = [error]; + parsedErrors.global = [createClerkGlobalHookError(error)]; return parsedErrors; } - error.errors.forEach(error => { - if (parsedErrors.raw) { - parsedErrors.raw.push(error); - } else { - parsedErrors.raw = [error]; - } - - if ('meta' in error && error.meta && 'paramName' in error.meta) { - const name = snakeToCamel(error.meta.paramName); - parsedErrors.fields[name as keyof typeof parsedErrors.fields] = error; - return; - } - - if (parsedErrors.global) { - parsedErrors.global.push(error); - } else { - parsedErrors.global = [error]; - } - }); + const hasFieldErrors = error.errors.some(error => 'meta' in error && error.meta && 'paramName' in error.meta); + if (hasFieldErrors) { + error.errors.forEach(error => { + if (parsedErrors.raw) { + parsedErrors.raw.push(error); + } else { + parsedErrors.raw = [error]; + } + if ('meta' in error && error.meta && 'paramName' in error.meta) { + const name = snakeToCamel(error.meta.paramName); + parsedErrors.fields[name as keyof typeof parsedErrors.fields] = error; + } + // Note that this assumes a given ClerkAPIResponseError will only have either field errors or global errors, but + // not both. If a global error is present, it will be discarded. + }); + + return parsedErrors; + } + + // At this point, we know that `error` is a ClerkAPIResponseError and that it has no field errors. + parsedErrors.raw = [error]; + parsedErrors.global = [createClerkGlobalHookError(error)]; return parsedErrors; } diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index 82fed3485ac..2fea2b45820 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -1,3 +1,4 @@ +import type { ClerkError } from '@clerk/shared/error'; import type { State as StateInterface } from '@clerk/shared/types'; import { computed, effect } from 'alien-signals'; @@ -36,7 +37,7 @@ export class State implements StateInterface { eventBus.on('resource:fetch', this.onResourceFetch); } - private onResourceError = (payload: { resource: BaseResource; error: unknown }) => { + private onResourceError = (payload: { resource: BaseResource; error: ClerkError | null }) => { if (payload.resource instanceof SignIn) { this.signInErrorSignal({ error: payload.error }); } diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 25ea370ca1c..14b32870d1f 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -2,6 +2,7 @@ export { errorToJSON, parseError, parseErrors } from './errors/parseError'; export { ClerkAPIError } from './errors/clerkApiError'; export { ClerkAPIResponseError } from './errors/clerkApiResponseError'; +export { ClerkError } from './errors/clerkError'; export { buildErrorThrower, type ErrorThrower, type ErrorThrowerOptions } from './errors/errorThrower'; @@ -27,3 +28,5 @@ export { isUnauthorizedError, isUserLockedError, } from './errors/helpers'; + +export { createClerkGlobalHookError } from './errors/globalHookError'; diff --git a/packages/shared/src/errors/clerkApiError.ts b/packages/shared/src/errors/clerkApiError.ts index 2ec91934187..42fc43183ee 100644 --- a/packages/shared/src/errors/clerkApiError.ts +++ b/packages/shared/src/errors/clerkApiError.ts @@ -14,15 +14,7 @@ export class ClerkAPIError implements Cler 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 { + const parsedError = { code: json.code, message: json.message, longMessage: json.long_message, @@ -36,6 +28,10 @@ export class ClerkAPIError implements Cler isPlanUpgradePossible: json.meta?.is_plan_upgrade_possible, } as unknown as Meta, }; + this.code = parsedError.code; + this.message = parsedError.message; + this.longMessage = parsedError.longMessage; + this.meta = parsedError.meta; } } diff --git a/packages/shared/src/errors/globalHookError.ts b/packages/shared/src/errors/globalHookError.ts index c4615e16234..888812660c5 100644 --- a/packages/shared/src/errors/globalHookError.ts +++ b/packages/shared/src/errors/globalHookError.ts @@ -1,5 +1,5 @@ -import { isClerkApiResponseError } from './clerkApiResponseError'; import type { ClerkError } from './clerkError'; +import { isClerkApiResponseError } from './clerkApiResponseError'; import { isClerkRuntimeError } from './clerkRuntimeError'; /** From 59df41c2b3ad62972511fd9ffa294979aee53d11 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:26:47 -0600 Subject: [PATCH 3/4] chore(repo): Add changeset --- .changeset/moody-parks-scream.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/moody-parks-scream.md diff --git a/.changeset/moody-parks-scream.md b/.changeset/moody-parks-scream.md new file mode 100644 index 00000000000..15f44a5b8d5 --- /dev/null +++ b/.changeset/moody-parks-scream.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +[Experimental] Add types for errors used in new custom flow APIs From ccb61e379b50e25a88822b5c9639eddd77949520 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:32:13 -0600 Subject: [PATCH 4/4] chore(shared): lint imports --- packages/shared/src/errors/globalHookError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/errors/globalHookError.ts b/packages/shared/src/errors/globalHookError.ts index 888812660c5..c4615e16234 100644 --- a/packages/shared/src/errors/globalHookError.ts +++ b/packages/shared/src/errors/globalHookError.ts @@ -1,5 +1,5 @@ -import type { ClerkError } from './clerkError'; import { isClerkApiResponseError } from './clerkApiResponseError'; +import type { ClerkError } from './clerkError'; import { isClerkRuntimeError } from './clerkRuntimeError'; /**