Error Handling
Proper error handling is crucial for creating robust authentication flows that provide clear feedback to users while maintaining security. Scute provides comprehensive error handling utilities to help you manage various failure scenarios gracefully.
Overview
Scute's error handling system is designed to:
- Provide meaningful error messages that help users understand what went wrong
- Maintain security by not exposing sensitive information in error messages
- Enable proper logging for debugging and monitoring
- Support different error types for various authentication scenarios
- Offer retry mechanisms for transient failures
Error Types
Scute categorizes errors into several types to help you handle them appropriately:
Custom Scute Errors
- identifier-not-recognized: The provided identifier (email/username) is not recognized in the system
- identifier-already-exists: A user with this identifier already exists during registration
- identifier-invalid: The identifier format is invalid (e.g., malformed email)
- new-device: Authentication attempt from a new/unrecognized device
- login-required: User must be logged in to access the resource
- invalid-auth-token: The authentication token is invalid or expired
- unknown-sign-in: An unknown error occurred during sign-in
- invalid-magic-link: The magic link is invalid, expired, or already used
HTTP Errors
- 4xx Client Errors: Bad request, unauthorized, forbidden, not found errors
- 5xx Server Errors: Internal server errors, service unavailable
- 502, 503, 504: Network-related errors (Bad Gateway, Service Unavailable, Gateway Timeout)
WebAuthn Errors
- ERROR_CEREMONY_ABORTED: The WebAuthn ceremony was aborted by the user
- ERROR_INVALID_DOMAIN: The current domain is invalid for WebAuthn
- ERROR_INVALID_RP_ID: The Relying Party ID is invalid for this domain
- ERROR_INVALID_USER_ID_LENGTH: User ID must be between 1 and 64 characters
- ERROR_MALFORMED_PUBKEYCREDPARAMS: Public key credential parameters are malformed
- ERROR_AUTHENTICATOR_GENERAL_ERROR: General authenticator error
- ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT: Authenticator doesn't support discoverable credentials
- ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT: Authenticator doesn't support user verification
- ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: The authenticator was previously registered
- ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG: No supported algorithms in pubKeyCredParams
- ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY: Error passed through from platform (check cause property)
Technical Errors
- TechnicalError: Internal technical errors that should be handled gracefully
Basic Error Handling
Here's how to implement basic error handling with Scute:
import { getMeaningfulError, ScuteError } from "@scute/js-core";
const handleAuthError = (error: any) => {
const errorResult = getMeaningfulError(error);
// Log for debugging (include full error details)
console.error("Authentication error:", {
message: errorResult.message,
isFatal: errorResult.isFatal,
code: error.code,
timestamp: new Date().toISOString(),
stack: error.stack
});
// Show user-friendly message
setErrorMessage(
errorResult.message || "Authentication failed. Please try again."
);
// Handle specific error types
switch (error.code) {
case "identifier-not-recognized":
setErrorMessage("This email address is not recognized. Please check your email or sign up for a new account.");
// Show sign up option
setShowSignUpOption(true);
break;
case "identifier-already-exists":
setErrorMessage("An account with this email already exists. Please sign in instead.");
// Redirect to sign in
setShowSignInForm(true);
break;
case "identifier-invalid":
setErrorMessage("Please enter a valid email address.");
// Focus on email field
focusEmailField();
break;
case "new-device":
setErrorMessage("New device detected. Please check your email for verification.");
// Show device verification flow
setShowDeviceVerification(true);
break;
case "login-required":
setErrorMessage("Please sign in to continue.");
// Redirect to login
redirectToLogin();
break;
case "invalid-auth-token":
setErrorMessage("Your session has expired. Please sign in again.");
// Clear stored tokens and redirect
clearTokensAndRedirect();
break;
case "invalid-magic-link":
setErrorMessage("This magic link is invalid or has expired. Please request a new one.");
// Show magic link request form
setShowMagicLinkForm(true);
break;
case "ERROR_CEREMONY_ABORTED":
setErrorMessage("Authentication was cancelled. Please try again.");
// Re-enable authentication buttons
setIsAuthDisabled(false);
break;
case "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED":
setErrorMessage("This authenticator is already registered. Please use a different one or sign in.");
break;
case 502:
case 503:
case 504:
setErrorMessage("Service is temporarily unavailable. Please try again in a few moments.");
// Enable retry mechanism
setShowRetryButton(true);
break;
default:
// Handle fatal vs non-fatal errors
if (errorResult.isFatal) {
setErrorMessage("A technical error occurred. Please refresh the page and try again.");
setShowRefreshButton(true);
} else {
setErrorMessage(errorResult.message || "An unexpected error occurred. Please try again.");
}
}
};
Advanced Error Handling Patterns
Error Boundary for React Applications
Implement an error boundary to catch and handle authentication errors at the component level:
import React from 'react';
import { getMeaningfulError } from "@scute/js-core";
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class AuthErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
ErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<{}>) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error details for debugging
console.error('Auth Error Boundary caught an error:', {
error: getMeaningfulError(error),
errorInfo,
timestamp: new Date().toISOString()
});
// Send error to monitoring service
if (process.env.NODE_ENV === 'production') {
sendErrorToMonitoring(error, errorInfo);
}
}
render() {
if (this.state.hasError) {
return (
<div className="auth-error-fallback">
<h2>Authentication Error</h2>
<p>{getMeaningfulError(this.state.error)}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
Retry Logic with Exponential Backoff
Implement smart retry logic for transient errors:
import { ScuteAuth } from "@scute/js-core";
const authenticateWithRetry = async (
credentials: { email: string; password: string },
maxRetries: number = 3
) => {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await ScuteAuth.signIn(credentials);
return result;
} catch (error: any) {
lastError = error;
// Don't retry for certain error types that won't resolve with retries
if (error.code === "identifier-not-recognized" ||
error.code === "identifier-already-exists" ||
error.code === "identifier-invalid" ||
error.code === "invalid-magic-link" ||
error.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED" ||
error.code === "ERROR_INVALID_DOMAIN" ||
error.code === "ERROR_INVALID_RP_ID") {
throw error;
}
// Don't retry on the last attempt
if (attempt === maxRetries) {
break;
}
// Only retry for network errors and server errors
const shouldRetry = [502, 503, 504].includes(error.code) ||
error.code === "ERROR_CEREMONY_ABORTED" ||
error instanceof TechnicalError;
if (!shouldRetry) {
throw error;
}
// Exponential backoff: wait 2^attempt seconds
const delay = Math.pow(2, attempt) * 1000;
console.log(`Authentication attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
};
Form Validation with Error Display
Create a comprehensive form handler with error display:
import { useState } from 'react';
import { ScuteAuth, getMeaningfulError } from "@scute/js-core";
const useAuthForm = () => {
const [errors, setErrors] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const [generalError, setGeneralError] = useState<string>("");
const validateField = (name: string, value: string): string => {
switch (name) {
case 'email':
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Email format is invalid';
return '';
case 'password':
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
return '';
default:
return '';
}
};
const handleSubmit = async (formData: { email: string; password: string }) => {
// Clear previous errors
setErrors({});
setGeneralError("");
// Validate all fields
const fieldErrors: Record<string, string> = {};
Object.entries(formData).forEach(([key, value]) => {
const error = validateField(key, value);
if (error) fieldErrors[key] = error;
});
if (Object.keys(fieldErrors).length > 0) {
setErrors(fieldErrors);
return;
}
setIsLoading(true);
try {
await authenticateWithRetry(formData);
// Handle successful authentication
} catch (error: any) {
const errorResult = getMeaningfulError(error);
// Handle field-specific errors
if (error.code === "identifier-invalid") {
setErrors({ email: "Please enter a valid email address" });
} else if (error.code === "identifier-not-recognized") {
setErrors({ email: "This email address is not recognized" });
} else {
// Handle general errors
setGeneralError(errorResult.message || "Authentication failed. Please try again.");
}
// Log error for debugging
console.error("Form submission error:", {
error: errorResult.message,
isFatal: errorResult.isFatal,
code: error.code,
formData: { email: formData.email } // Don't log password
});
} finally {
setIsLoading(false);
}
};
return {
errors,
generalError,
isLoading,
handleSubmit,
validateField
};
};
Error Monitoring and Logging
Production Error Tracking
Set up comprehensive error tracking for production environments:
interface ErrorContext {
userId?: string;
sessionId?: string;
userAgent: string;
url: string;
timestamp: string;
}
const logAuthError = (error: any, context: Partial<ErrorContext> = {}) => {
const errorResult = getMeaningfulError(error);
const errorData = {
message: errorResult.message,
isFatal: errorResult.isFatal,
code: error.code,
stack: error.stack,
context: {
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString(),
...context
}
};
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Auth Error:', errorData);
}
// Send to monitoring service in production
if (process.env.NODE_ENV === 'production') {
// Example: Send to your monitoring service
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
}).catch(err => {
console.error('Failed to log error:', err);
});
}
};
Best Practices
1. User-Friendly Messages
Always provide clear, actionable error messages to users:
// ❌ Bad: Technical error exposed to user
setErrorMessage("JWT token validation failed: signature mismatch");
// ✅ Good: User-friendly message with clear action
setErrorMessage("Your session has expired. Please sign in again.");
2. Security Considerations
Never expose sensitive information in error messages:
// ❌ Bad: Reveals whether email exists
if (userNotFound) {
throw new Error("User with email john@example.com not found");
}
// ✅ Good: Generic message for security
if (invalidCredentials) {
throw new Error("Invalid email or password");
}
3. Graceful Degradation
Provide fallback functionality when possible:
const handleAuthError = (error: any) => {
if (error.code === "NETWORK_ERROR") {
// Offer offline mode or cached data
setOfflineMode(true);
setErrorMessage("You're offline. Some features may be limited.");
}
};
4. Error Recovery
Always provide users with a path to recover from errors:
const ErrorDisplay = ({ error, onRetry, onReset }) => (
<div className="error-container">
<p>{getMeaningfulError(error)}</p>
<div className="error-actions">
<button onClick={onRetry}>Try Again</button>
<button onClick={onReset}>Reset Form</button>
<a href="/forgot-password">Forgot Password?</a>
</div>
</div>
);
Testing Error Scenarios
Create comprehensive tests for your error handling:
import { describe, it, expect, vi } from 'vitest';
import { getMeaningfulError } from '@scute/js-core';
describe('Error Handling', () => {
it('should handle identifier not recognized error', () => {
const error = { code: 'identifier-not-recognized', message: 'Identifier is not recognized.' };
const result = getMeaningfulError(error);
expect(result.message).toBe('Identifier is not recognized.');
expect(result.isFatal).toBe(false);
});
it('should handle network errors as fatal', () => {
const error = { code: 502, message: 'Bad Gateway' };
const result = getMeaningfulError(error);
expect(result.isFatal).toBe(true);
});
it('should handle WebAuthn ceremony aborted as non-fatal', () => {
const error = { code: 'ERROR_CEREMONY_ABORTED', message: 'Ceremony aborted' };
const result = getMeaningfulError(error);
expect(result.isFatal).toBe(false);
});
it('should handle technical errors as fatal', () => {
const technicalError = new TechnicalError();
const result = getMeaningfulError(technicalError);
expect(result.isFatal).toBe(true);
expect(result.message).toBe('Something went wrong.');
});
it('should handle custom scute errors as non-fatal', () => {
const error = { code: 'invalid-magic-link', message: 'Invalid magic link.' };
const result = getMeaningfulError(error);
expect(result.message).toBe('Invalid magic link.');
expect(result.isFatal).toBe(false);
});
});
By implementing comprehensive error handling following these patterns, you'll create a robust authentication system that provides excellent user experience even when things go wrong.