or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

backend-api-client.mdclient-components.mdclient-hooks.mderror-handling.mdindex.mdmiddleware-and-route-protection.mdserver-auth-app-router.mdserver-auth-pages-router.mdsetup-and-provider.mdwebhooks.md
tile.json

error-handling.mddocs/

Error Handling

Utilities for detecting and handling different types of Clerk errors. These error detection functions help you handle specific error scenarios in your application.

Key Information for Agents

Required Setup:

  • Error utilities imported from @clerk/nextjs/errors
  • Error detection functions return boolean (don't throw)
  • Error handling should use try/catch blocks around Clerk operations

Default Behaviors:

  • All error detection functions return boolean (true if error matches type)
  • Functions don't throw errors (safe to call without try/catch)
  • ClerkAPIResponseError has status, errors array, and clerkError properties
  • Error codes are string literals (e.g., 'form_identifier_not_found')
  • EmailLinkErrorCode constants available for email link error codes
  • Error detection is type-safe (TypeScript narrows error types)

Threading Model:

  • Error detection functions are synchronous (return immediately)
  • Functions can be called in any context (client or server)
  • No async operations (no Promises or await needed)
  • Thread-safe: No shared mutable state

Lifecycle:

  • Errors are thrown by Clerk operations (API calls, authentication, etc.)
  • Error detection functions check error type
  • Error handling logic processes specific error types
  • User-friendly error messages displayed to user
  • Error logging for debugging and monitoring

Edge Cases:

  • Unknown errors: Use isKnownError() to check if error is Clerk-related
  • Multiple error types: Check most specific first, then general
  • Error codes: Access via err.errors[0]?.code for ClerkAPIResponseError
  • Nested errors: Error detection works on error objects directly
  • Error messages: Access via err.errors[0]?.message for user-friendly messages
  • Error metadata: Some errors have meta property with additional context

Exceptions:

  • Error detection functions never throw (always return boolean)
  • Invalid error objects return false (not Clerk error)
  • Missing error properties cause detection to return false
  • Type narrowing: TypeScript narrows error type after detection function returns true

Capabilities

Error Detection Functions

Functions to identify specific types of Clerk errors.

/**
 * Checks if error is a Clerk API response error
 * API errors occur when Clerk backend returns an error response
 * @param err - Error to check
 * @returns True if error is ClerkAPIResponseError
 */
function isClerkAPIResponseError(err: unknown): boolean;

/**
 * Checks if error is a Clerk runtime error
 * Runtime errors occur during Clerk SDK operations
 * @param err - Error to check
 * @returns True if error is ClerkRuntimeError
 */
function isClerkRuntimeError(err: unknown): boolean;

/**
 * Checks if error is an email link error
 * Email link errors occur during email verification flows
 * @param err - Error to check
 * @returns True if error is EmailLinkError
 */
function isEmailLinkError(err: unknown): boolean;

/**
 * Checks if error is a known Clerk error
 * Known errors include all Clerk-specific error types
 * @param err - Error to check
 * @returns True if error is a known Clerk error
 */
function isKnownError(err: unknown): boolean;

/**
 * Checks if error is a reverification cancelled error
 * Occurs when user cancels reverification flow
 * @param err - Error to check
 * @returns True if error is reverification cancelled
 */
function isReverificationCancelledError(err: unknown): boolean;

/**
 * Checks if error is a Metamask-related error
 * Occurs during Metamask authentication flows
 * @param err - Error to check
 * @returns True if error is MetamaskError
 */
function isMetamaskError(err: unknown): boolean;

Usage Example - API Route Error Handling:

import {
  isClerkAPIResponseError,
  isClerkRuntimeError,
  isKnownError,
} from '@clerk/nextjs/errors';
import { clerkClient } from '@clerk/nextjs/server';

export async function POST(req: Request) {
  try {
    const { userId } = await req.json();
    const client = await clerkClient();
    const user = await client.users.getUser(userId);

    return Response.json({ user });
  } catch (err) {
    // Check if it's a Clerk API error
    if (isClerkAPIResponseError(err)) {
      console.error('Clerk API error:', err);
      return Response.json(
        {
          error: 'Failed to fetch user from Clerk',
          details: err.errors,
        },
        { status: err.status }
      );
    }

    // Check if it's a Clerk runtime error
    if (isClerkRuntimeError(err)) {
      console.error('Clerk runtime error:', err);
      return Response.json(
        { error: 'Clerk SDK error' },
        { status: 500 }
      );
    }

    // Check if it's any known Clerk error
    if (isKnownError(err)) {
      console.error('Known Clerk error:', err);
      return Response.json(
        { error: 'Clerk operation failed' },
        { status: 400 }
      );
    }

    // Unknown error
    console.error('Unknown error:', err);
    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Usage Example - Client Component Error Handling:

'use client';

import { useSignIn } from '@clerk/nextjs';
import {
  isClerkAPIResponseError,
  isKnownError,
} from '@clerk/nextjs/errors';
import { useState } from 'react';

export default function SignInForm() {
  const { signIn, setActive } = useSignIn();
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setError(null);

    const formData = new FormData(e.currentTarget);
    const email = formData.get('email') as string;
    const password = formData.get('password') as string;

    try {
      const result = await signIn?.create({
        identifier: email,
        password,
      });

      if (result?.status === 'complete') {
        await setActive({ session: result.createdSessionId });
      }
    } catch (err) {
      // Handle Clerk API errors
      if (isClerkAPIResponseError(err)) {
        if (err.errors[0]?.code === 'form_identifier_not_found') {
          setError('Email not found');
        } else if (err.errors[0]?.code === 'form_password_incorrect') {
          setError('Incorrect password');
        } else {
          setError('Sign in failed. Please try again.');
        }
        return;
      }

      // Handle other known errors
      if (isKnownError(err)) {
        setError('An error occurred. Please try again.');
        return;
      }

      // Unknown error
      console.error('Unknown error:', err);
      setError('An unexpected error occurred');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Sign In</button>
    </form>
  );
}

ClerkAPIResponseError Structure

When using isClerkAPIResponseError, the error has specific properties:

/**
 * Clerk API Response Error structure
 */
interface ClerkAPIResponseError extends Error {
  /**
   * HTTP status code
   */
  status: number;

  /**
   * Error code from Clerk
   */
  clerkError: boolean;

  /**
   * Array of error details
   */
  errors: ClerkAPIError[];
}

interface ClerkAPIError {
  /**
   * Error code (e.g., 'form_identifier_not_found')
   */
  code: string;

  /**
   * Human-readable error message
   */
  message: string;

  /**
   * Long-form error message
   */
  longMessage?: string;

  /**
   * Field that caused the error (if applicable)
   */
  meta?: {
    paramName?: string;
  };
}

Usage Example - Detailed Error Handling:

import { isClerkAPIResponseError } from '@clerk/nextjs/errors';
import { clerkClient } from '@clerk/nextjs/server';

export async function POST(req: Request) {
  try {
    const { email } = await req.json();
    const client = await clerkClient();

    const user = await client.users.createUser({
      emailAddress: [email],
    });

    return Response.json({ user });
  } catch (err) {
    if (isClerkAPIResponseError(err)) {
      // Log all error details
      console.error('Clerk API Error:', {
        status: err.status,
        errors: err.errors.map(e => ({
          code: e.code,
          message: e.message,
          field: e.meta?.paramName,
        })),
      });

      // Handle specific error codes
      const errorCode = err.errors[0]?.code;

      if (errorCode === 'form_param_format_invalid') {
        return Response.json(
          { error: 'Invalid email format' },
          { status: 400 }
        );
      }

      if (errorCode === 'form_identifier_exists') {
        return Response.json(
          { error: 'Email already exists' },
          { status: 400 }
        );
      }

      // Generic API error response
      return Response.json(
        {
          error: err.errors[0]?.message || 'API request failed',
        },
        { status: err.status }
      );
    }

    // Unknown error
    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Email Link Error Codes

Email link errors occur during email verification flows.

/**
 * Email link error codes
 */
const EmailLinkErrorCode: {
  /**
   * Email link has expired
   */
  Expired: 'expired';

  /**
   * Email link has already been used
   */
  Failed: 'failed';

  /**
   * Email link is not found
   */
  NotFound: 'not_found';

  /**
   * Email verification is not supported
   */
  NotSupported: 'not_supported';

  /**
   * Client mismatch error
   */
  ClientMismatch: 'client_mismatch';
};

/**
 * Email link error code status mapping
 */
const EmailLinkErrorCodeStatus: {
  expired: number;
  failed: number;
  not_found: number;
  not_supported: number;
  client_mismatch: number;
};

Usage Example - Email Link Error Handling:

'use client';

import { useSignUp } from '@clerk/nextjs';
import {
  isEmailLinkError,
  EmailLinkErrorCode,
} from '@clerk/nextjs/errors';
import { useEffect, useState } from 'react';

export default function EmailVerificationPage() {
  const { signUp } = useSignUp();
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const verifyEmailLink = async () => {
      try {
        // Verify email link from URL
        await signUp?.attemptEmailAddressVerification({
          code: 'email_link',
        });
      } catch (err) {
        if (isEmailLinkError(err)) {
          // Handle specific email link errors
          if (err.code === EmailLinkErrorCode.Expired) {
            setError('Email verification link has expired. Please request a new one.');
          } else if (err.code === EmailLinkErrorCode.Failed) {
            setError('Email verification failed. Please try again.');
          } else if (err.code === EmailLinkErrorCode.NotFound) {
            setError('Email verification link not found.');
          } else if (err.code === EmailLinkErrorCode.ClientMismatch) {
            setError('Please open the link in the same browser where you started sign-up.');
          } else {
            setError('Email verification error. Please try again.');
          }
        } else {
          setError('An unexpected error occurred.');
        }
      }
    };

    verifyEmailLink();
  }, [signUp]);

  if (error) {
    return <div className="error">{error}</div>;
  }

  return <div>Verifying email...</div>;
}

Reverification Error Handling

Handle reverification cancellation errors.

/**
 * Reverification cancelled error
 * Occurs when user cancels reverification flow
 */
interface ReverificationCancelledError extends Error {
  code: 'reverification_cancelled';
}

Usage Example - Reverification Error:

'use client';

import { useUser } from '@clerk/nextjs';
import { isReverificationCancelledError } from '@clerk/nextjs/errors';

export default function SensitiveAction() {
  const { user } = useUser();

  const handleSensitiveAction = async () => {
    try {
      // Perform action that requires reverification
      await user?.update({
        password: 'new-password',
      });
    } catch (err) {
      if (isReverificationCancelledError(err)) {
        console.log('User cancelled reverification');
        // Don't show error, just cancel the action
        return;
      }

      // Handle other errors
      console.error('Action failed:', err);
      alert('Failed to update password');
    }
  };

  return (
    <button onClick={handleSensitiveAction}>
      Change Password
    </button>
  );
}

Metamask Error Handling

Handle Metamask-specific authentication errors.

/**
 * Metamask error
 * Occurs during Metamask authentication flows
 */
interface MetamaskError extends Error {
  code: string;
  metamaskError: boolean;
}

Usage Example - Metamask Error:

'use client';

import { useClerk } from '@clerk/nextjs';
import { isMetamaskError } from '@clerk/nextjs/errors';

export default function MetamaskSignIn() {
  const { authenticateWithMetamask } = useClerk();

  const handleMetamaskSignIn = async () => {
    try {
      await authenticateWithMetamask();
    } catch (err) {
      if (isMetamaskError(err)) {
        if (err.code === 'metamask_not_installed') {
          alert('Please install Metamask to continue');
        } else if (err.code === 'metamask_user_rejected') {
          console.log('User rejected Metamask connection');
        } else {
          alert('Metamask authentication failed');
        }
        return;
      }

      // Handle other errors
      console.error('Authentication error:', err);
      alert('Failed to authenticate with Metamask');
    }
  };

  return (
    <button onClick={handleMetamaskSignIn}>
      Sign in with Metamask
    </button>
  );
}

Common Error Codes

Clerk API errors include specific error codes for different scenarios:

/**
 * Common Clerk error codes
 */
type ClerkErrorCode =
  // Form validation errors
  | 'form_param_format_invalid'
  | 'form_param_missing'
  | 'form_param_nil'
  | 'form_param_value_invalid'
  | 'form_param_unknown'
  | 'form_param_max_length_exceeded'
  | 'form_param_min_length_not_met'

  // Authentication errors
  | 'form_identifier_not_found'
  | 'form_password_incorrect'
  | 'form_identifier_exists'
  | 'session_exists'
  | 'not_allowed_to_sign_up'

  // Resource errors
  | 'resource_not_found'
  | 'resource_invalid'

  // Authorization errors
  | 'authorization_invalid'
  | 'client_state_invalid'

  // Rate limiting
  | 'rate_limit_exceeded'

  // Two-factor authentication
  | 'verification_required'
  | 'verification_expired'
  | 'verification_failed'

  // Organization errors
  | 'organization_domain_common'
  | 'organization_invitation_already_accepted'

  // Billing errors
  | 'billing_invalid_subscription'
  | 'billing_quota_exceeded';

Usage Example - Error Code Mapping:

import { isClerkAPIResponseError } from '@clerk/nextjs/errors';

function getErrorMessage(err: unknown): string {
  if (!isClerkAPIResponseError(err)) {
    return 'An unexpected error occurred';
  }

  const code = err.errors[0]?.code;

  const errorMessages: Record<string, string> = {
    form_identifier_not_found: 'Email or username not found',
    form_password_incorrect: 'Incorrect password',
    form_identifier_exists: 'This email is already registered',
    form_param_format_invalid: 'Invalid format',
    rate_limit_exceeded: 'Too many attempts. Please try again later.',
    verification_expired: 'Verification code expired. Please request a new one.',
    verification_failed: 'Invalid verification code',
    not_allowed_to_sign_up: 'Sign up is not allowed',
    resource_not_found: 'Resource not found',
    authorization_invalid: 'Unauthorized',
  };

  return errorMessages[code] || err.errors[0]?.message || 'Request failed';
}

// Usage in error handler
export async function POST(req: Request) {
  try {
    // ... Clerk operation
  } catch (err) {
    const message = getErrorMessage(err);
    return Response.json({ error: message }, { status: 400 });
  }
}

Error Handling Best Practices

1. Graceful Degradation

Handle errors gracefully without breaking user experience:

'use client';

import { useUser } from '@clerk/nextjs';
import { isClerkAPIResponseError } from '@clerk/nextjs/errors';
import { useState } from 'react';

export default function UserProfile() {
  const { user } = useUser();
  const [error, setError] = useState<string | null>(null);
  const [isUpdating, setIsUpdating] = useState(false);

  const updateProfile = async (data: any) => {
    setIsUpdating(true);
    setError(null);

    try {
      await user?.update(data);
      alert('Profile updated successfully');
    } catch (err) {
      if (isClerkAPIResponseError(err)) {
        setError(err.errors[0]?.message || 'Update failed');
      } else {
        setError('An unexpected error occurred');
      }
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <div>
      {error && <div className="error">{error}</div>}
      <button
        onClick={() => updateProfile({ firstName: 'John' })}
        disabled={isUpdating}
      >
        Update Profile
      </button>
    </div>
  );
}

2. User-Friendly Messages

Convert technical errors to user-friendly messages:

import { isClerkAPIResponseError } from '@clerk/nextjs/errors';

function getUserFriendlyError(err: unknown): string {
  if (!isClerkAPIResponseError(err)) {
    return 'Something went wrong. Please try again.';
  }

  const code = err.errors[0]?.code;

  // Map technical codes to friendly messages
  switch (code) {
    case 'form_identifier_not_found':
      return "We couldn't find an account with that email.";
    case 'form_password_incorrect':
      return 'The password you entered is incorrect.';
    case 'form_identifier_exists':
      return 'An account with this email already exists.';
    case 'rate_limit_exceeded':
      return "You've made too many attempts. Please wait a few minutes and try again.";
    default:
      return err.errors[0]?.message || 'Something went wrong. Please try again.';
  }
}

3. Logging and Monitoring

Log errors for debugging and monitoring:

import { isClerkAPIResponseError, isKnownError } from '@clerk/nextjs/errors';

function logError(err: unknown, context: string) {
  if (isClerkAPIResponseError(err)) {
    console.error(`[${context}] Clerk API Error:`, {
      status: err.status,
      errors: err.errors,
      timestamp: new Date().toISOString(),
    });
  } else if (isKnownError(err)) {
    console.error(`[${context}] Known Clerk Error:`, {
      error: err,
      timestamp: new Date().toISOString(),
    });
  } else {
    console.error(`[${context}] Unknown Error:`, {
      error: err,
      timestamp: new Date().toISOString(),
    });
  }

  // Send to error tracking service
  // trackError(err, context);
}

// Usage
export async function POST(req: Request) {
  try {
    // ... operation
  } catch (err) {
    logError(err, 'POST /api/users');
    return Response.json({ error: 'Request failed' }, { status: 500 });
  }
}

4. Retry Logic

Implement retry logic for transient errors:

import { isClerkAPIResponseError } from '@clerk/nextjs/errors';

async function retryOperation<T>(
  operation: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  let lastError: unknown;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await operation();
    } catch (err) {
      lastError = err;

      // Don't retry on client errors (4xx)
      if (isClerkAPIResponseError(err) && err.status < 500) {
        throw err;
      }

      // Wait before retrying (exponential backoff)
      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
      }
    }
  }

  throw lastError;
}

// Usage
export async function POST(req: Request) {
  try {
    const result = await retryOperation(async () => {
      const client = await clerkClient();
      return await client.users.getUser('user_123');
    });

    return Response.json({ result });
  } catch (err) {
    return Response.json({ error: 'Operation failed' }, { status: 500 });
  }
}

Import Location

All error utilities are exported from @clerk/nextjs/errors:

import {
  isClerkAPIResponseError,
  isClerkRuntimeError,
  isEmailLinkError,
  isKnownError,
  isReverificationCancelledError,
  isMetamaskError,
  EmailLinkErrorCode,
  EmailLinkErrorCodeStatus,
} from '@clerk/nextjs/errors';