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
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
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;