Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createEventBus } from '@clerk/shared/eventBus';
import type { TokenResource } from '@clerk/types';
import type { ClerkAPIResponseError, TokenResource } from '@clerk/types';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify all emitters conform to the new payload

Now that ResourceErrorPayload.error is ClerkAPIResponseError | null, ensure all eventBus.emit('resource:error', …) sites either pass a properly typed API error or null (for non-API errors).

Run this to find non-conforming emitters:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "All resource:error emit sites:"
rg -n "eventBus\\.emit\\('resource:error'" -A 3 -B 1

echo
echo "Emitters that don't cast/guard to ClerkAPIResponseError or null:"
rg -n "eventBus\\.emit\\('resource:error', \\{[^}]*error: (?!null)(?!\\(.*ClerkAPIResponseError\\)).*\\}" -U || true

Length of output: 5359


Ensure all resource:error emits use ClerkAPIResponseError | null

A couple of eventBus.emit('resource:error', …) calls are still passing the raw err (type unknown) instead of casting it to ClerkAPIResponseError. Please update them to match the new payload type:

• packages/clerk-js/src/core/resources/SignIn.ts

  • Line 522
  • Line 535

Change each instance from:

eventBus.emit('resource:error', { resource: this.resource, error: err });

to:

eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });

This ensures ResourceErrorPayload.error is always ClerkAPIResponseError or null.

🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts at lines 522 and 535, the
eventBus.emit calls for 'resource:error' are passing the error as type unknown
instead of ClerkAPIResponseError. Update these calls by casting the error to
ClerkAPIResponseError using 'err as ClerkAPIResponseError' to ensure the emitted
payload matches the expected type ClerkAPIResponseError | null.


import type { BaseResource } from './resources/Base';

Expand All @@ -14,7 +14,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: ClerkAPIResponseError | null };

type InternalEvents = {
[events.TokenUpdate]: TokenUpdatePayload;
Expand Down
7 changes: 4 additions & 3 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
AuthenticateWithPopupParams,
AuthenticateWithRedirectParams,
AuthenticateWithWeb3Params,
ClerkAPIResponseError,
CreateEmailLinkFlowReturn,
EmailCodeConfig,
EmailLinkConfig,
Expand Down Expand Up @@ -557,7 +558,7 @@ class SignInFuture implements SignInFutureResource {
action: 'prepare_first_factor',
});
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });
return { error: err };
}
Comment on lines 560 to 562

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid unsound cast; guard before emitting API error

err here can be a locally thrown Error (e.g., “Email code factor not found”). Blindly casting to ClerkAPIResponseError risks runtime shape errors down the line. Guard, and emit null for non-API errors.

Apply:

-      eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });
+      const apiErr = isClerkAPIResponseError(err) ? err : null;
+      eventBus.emit('resource:error', { resource: this.resource, error: apiErr });

Add this helper (once per module or in a shared util):

// Type guard for API errors
function isClerkAPIResponseError(err: unknown): err is ClerkAPIResponseError {
  return !!err && typeof err === 'object' && Array.isArray((err as any).errors);
}
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts around lines 560 to 563, the
error object `err` is unsafely cast to `ClerkAPIResponseError` without checking
its type, which can cause runtime errors. To fix this, implement a type guard
function `isClerkAPIResponseError` that checks if `err` has the expected shape
(e.g., an `errors` array). Use this guard before emitting the error event: emit
the error only if it passes the guard, otherwise emit null or handle
accordingly. This prevents unsafe casts and ensures only valid API errors are
emitted.


Expand All @@ -572,7 +573,7 @@ class SignInFuture implements SignInFutureResource {
action: 'attempt_first_factor',
});
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });
return { error: err };
}
Comment on lines 575 to 577

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same guard needed here

Don’t cast arbitrary errors to ClerkAPIResponseError; guard and emit null when not API-shaped.

-      eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });
+      const apiErr = isClerkAPIResponseError(err) ? err : null;
+      eventBus.emit('resource:error', { resource: this.resource, error: apiErr });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });
return { error: err };
}
} catch (err: unknown) {
const apiErr = isClerkAPIResponseError(err) ? err : null;
eventBus.emit('resource:error', { resource: this.resource, error: apiErr });
return { error: err };
}
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts around lines 575 to 578, the
catch block casts any caught error to ClerkAPIResponseError without verifying
its shape. To fix this, add a type guard to check if the error matches the
ClerkAPIResponseError structure before emitting it; if it does not, emit null
instead. Also, update the returned error accordingly to avoid unsafe casting.


Expand Down Expand Up @@ -612,7 +613,7 @@ class SignInFuture implements SignInFutureResource {
windowNavigate(externalVerificationRedirectURL);
}
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });
return { error: err };
Comment on lines 615 to 616

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard before emitting in SSO flow

This path throws local errors (e.g., “modal flow is not supported yet”). Use the same guard to avoid mis-typing the signal.

-      eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });
+      const apiErr = isClerkAPIResponseError(err) ? err : null;
+      eventBus.emit('resource:error', { resource: this.resource, error: apiErr });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
eventBus.emit('resource:error', { resource: this.resource, error: err as ClerkAPIResponseError });
return { error: err };
} catch (err: unknown) {
const apiErr = isClerkAPIResponseError(err) ? err : null;
eventBus.emit('resource:error', { resource: this.resource, error: apiErr });
return { error: err };
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts around lines 615 to 617, the
catch block emits an error event without checking if the current flow supports
it, which can cause issues with local errors like "modal flow is not supported
yet." Add a guard condition before emitting the 'resource:error' event to ensure
it only emits for supported flows, preventing mis-typing or inappropriate signal
emission.

}

Expand Down
69 changes: 66 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,80 @@
import type { ClerkAPIResponseError, Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: ClerkAPIResponseError | null }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

export function errorsToParsedErrors(error: ClerkAPIResponseError | null): Errors {
const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: error ? error.errors : [],
global: [],
};

error?.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
}
Comment on lines +53 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Field errors may be overwritten when multiple errors exist for the same field

The current implementation assigns errors directly to field properties (e.g., parsedErrors.fields.firstName = error). If the API returns multiple errors for the same field, only the last one will be retained, potentially losing important validation information.

Consider either:

  1. Changing field properties to arrays to store multiple errors per field
  2. Documenting this limitation if it's intentional
  3. Logging a warning when overwriting occurs

Example of potential data loss:

// If API returns two errors for 'first_name':
// Error 1: "First name is required"
// Error 2: "First name contains invalid characters"
// Only Error 2 would be retained in parsedErrors.fields.firstName
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 53 to 89, the code assigns
a single error object to each field which overwrites earlier errors for the same
field; change each field property to hold an array of errors (or, if the
surrounding types must remain, convert to an array when a second error appears)
and push errors instead of assigning so all errors are preserved; update the
parsedErrors initialization and related types/interfaces to use arrays for
fields (or handle conversion on conflict), and ensure downstream callers
expect/handle arrays (or convert them back if necessary).

} else {
parsedErrors.global.push(error);
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return parsedErrors;
}
Comment on lines +27 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add unit tests for the new error parsing functionality

The errorsToParsedErrors function introduces complex error parsing logic with multiple branches and field mappings. Please add comprehensive unit tests to cover:

  • Non-Clerk API errors
  • Clerk API errors with various paramName values
  • Errors without meta or paramName properties
  • Multiple errors for the same field
  • Unknown paramName values hitting the default case
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 27 to 96, add
comprehensive unit tests for errorsToParsedErrors to validate its branching and
field mapping logic: create test cases for (1) non-Clerk errors (ensure raw and
global contain the original error), (2) Clerk API errors with each supported
paramName (first_name, last_name, email_address, identifier, phone_number,
password, username, code, captcha, legal_accepted) mapping to the correct
fields, (3) errors missing meta or paramName (expect them in global), (4)
multiple errors for the same field (ensure the last/expected assignment or
accumulation behavior asserted), and (5) unknown paramName values hitting the
default case (assigned to global); implement assertions on parsedErrors.fields,
parsedErrors.raw, and parsedErrors.global for each case.

6 changes: 3 additions & 3 deletions packages/clerk-js/src/core/state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { State as StateInterface } from '@clerk/types';
import type { ClerkAPIResponseError, State as StateInterface } from '@clerk/types';
import { computed, effect } from 'alien-signals';

import { eventBus } from './events';
Expand All @@ -19,9 +19,9 @@ export class State implements StateInterface {
eventBus.on('resource:error', this.onResourceError);
}

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
private onResourceError = (payload: { resource: BaseResource; error: ClerkAPIResponseError | null }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';
 
-function useClerkSignal(signal: 'signIn') {
+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
   …
 }
 
-export function useSignInSignal() {
+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
   return useClerkSignal('signIn');
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function useClerkSignal(signal: 'signIn') {
// Add the type import
import type { State as ClerkState } from '@clerk/types';
function useClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']> | null {
}
export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: any[];
global: any[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
Loading