Error Handling
Handle errors gracefully in your application
This guide covers error handling patterns for the Arky SDK.
Result Type
All SDK methods return a Result type that explicitly handles success and failure:
const result = await sdk.eshop.getProduct({
businessId: 'biz_abc123',
id: 'prod_xyz'
});
if (result.ok) {
// Success - result.val contains the data
const product = result.val;
console.log(product.name);
} else {
// Error - result.val contains error details
const error = result.val;
console.error(error.message);
}
Error Structure
Errors follow a consistent structure:
interface ApiError {
error: string; // Error code (e.g., "NOT_FOUND")
message: string; // Human-readable message
details?: { // Additional context
field?: string;
reason?: string;
};
statusCode: number; // HTTP status code
}
Common Error Codes
Authentication Errors (401)
| Code | Description |
|---|---|
UNAUTHORIZED | Missing or invalid token |
TOKEN_EXPIRED | Access token has expired |
INVALID_CREDENTIALS | Wrong email/password |
USER_NOT_CONFIRMED | Email not verified |
USER_DISABLED | Account has been disabled |
Authorization Errors (403)
| Code | Description |
|---|---|
FORBIDDEN | Insufficient permissions |
BUSINESS_ACCESS_DENIED | No access to this business |
Not Found Errors (404)
| Code | Description |
|---|---|
NOT_FOUND | Resource doesn’t exist |
PRODUCT_NOT_FOUND | Product not found |
ORDER_NOT_FOUND | Order not found |
USER_NOT_FOUND | User not found |
Validation Errors (400)
| Code | Description |
|---|---|
VALIDATION_ERROR | Invalid input data |
INVALID_EMAIL | Invalid email format |
WEAK_PASSWORD | Password too weak |
MISSING_FIELD | Required field missing |
INVALID_PROMO_CODE | Promo code invalid or expired |
Conflict Errors (409)
| Code | Description |
|---|---|
DUPLICATE_EMAIL | Email already registered |
DUPLICATE_SLUG | Slug already in use |
SLOT_UNAVAILABLE | Time slot no longer available |
INSUFFICIENT_INVENTORY | Not enough stock |
Payment Errors (402)
| Code | Description |
|---|---|
PAYMENT_FAILED | Payment processing failed |
CARD_DECLINED | Card was declined |
INSUFFICIENT_FUNDS | Insufficient funds |
REQUIRES_ACTION | 3D Secure required |
Handling Patterns
Basic Error Handling
async function getProduct(id: string) {
const result = await sdk.eshop.getProduct({
businessId: 'biz_abc123',
id
});
if (!result.ok) {
const error = result.val;
switch (error.error) {
case 'NOT_FOUND':
throw new NotFoundError('Product not found');
case 'UNAUTHORIZED':
redirectToLogin();
return null;
default:
throw new Error(error.message);
}
}
return result.val;
}
Form Validation Errors
async function handleRegister(formData: RegisterForm) {
const result = await sdk.user.registerUser({
...formData,
provider: 'EMAIL'
});
if (!result.ok) {
const error = result.val;
// Map API errors to form fields
if (error.error === 'VALIDATION_ERROR') {
return {
success: false,
fieldErrors: {
[error.details?.field || 'form']: error.message
}
};
}
if (error.error === 'DUPLICATE_EMAIL') {
return {
success: false,
fieldErrors: {
email: 'This email is already registered'
}
};
}
return {
success: false,
formError: error.message
};
}
return { success: true, user: result.val };
}
React Error Component
function useApiCall<T>(
apiCall: () => Promise<Result<T, ApiError>>
) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<ApiError | null>(null);
const [loading, setLoading] = useState(false);
const execute = async () => {
setLoading(true);
setError(null);
const result = await apiCall();
if (result.ok) {
setData(result.val);
} else {
setError(result.val);
}
setLoading(false);
};
return { data, error, loading, execute };
}
// Usage
function ProductPage({ id }) {
const { data: product, error, loading, execute } = useApiCall(() =>
sdk.eshop.getProduct({ businessId: 'biz_123', id })
);
useEffect(() => { execute(); }, [id]);
if (loading) return <Spinner />;
if (error) return <ErrorDisplay error={error} onRetry={execute} />;
if (!product) return null;
return <Product data={product} />;
}
function ErrorDisplay({ error, onRetry }) {
return (
<div className="error-container">
<h2>Something went wrong</h2>
<p>{error.message}</p>
{onRetry && (
<button onClick={onRetry}>Try Again</button>
)}
</div>
);
}
Global Error Handler
// Create SDK with error handling
const sdk = createArkyClient({
businessId: process.env.ARKY_BUSINESS_ID!,
getToken: () => localStorage.getItem('arky_token'),
setToken: (token) => localStorage.setItem('arky_token', token),
onAuthError: () => {
// Clear token and redirect to login
localStorage.removeItem('arky_token');
window.location.href = '/login?expired=true';
}
});
Retry Logic
async function withRetry<T>(
fn: () => Promise<Result<T, ApiError>>,
maxRetries = 3,
delay = 1000
): Promise<Result<T, ApiError>> {
let lastError: ApiError | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const result = await fn();
if (result.ok) return result;
lastError = result.val;
// Don't retry client errors
if (lastError.statusCode >= 400 && lastError.statusCode < 500) {
return result;
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
}
return { ok: false, val: lastError! };
}
// Usage
const result = await withRetry(() =>
sdk.eshop.getProducts({ businessId: 'biz_123', limit: 20 })
);
User-Friendly Messages
Map error codes to user-friendly messages:
const ERROR_MESSAGES: Record<string, string> = {
// Auth
INVALID_CREDENTIALS: 'Invalid email or password. Please try again.',
USER_NOT_CONFIRMED: 'Please check your email to verify your account.',
TOKEN_EXPIRED: 'Your session has expired. Please log in again.',
// Products
PRODUCT_NOT_FOUND: 'This product is no longer available.',
INSUFFICIENT_INVENTORY: 'Sorry, this item is out of stock.',
// Orders
PAYMENT_FAILED: 'Payment failed. Please check your card details.',
CARD_DECLINED: 'Your card was declined. Please try another payment method.',
INVALID_PROMO_CODE: 'This promo code is invalid or has expired.',
// Reservations
SLOT_UNAVAILABLE: 'This time slot is no longer available. Please choose another.',
// Default
DEFAULT: 'Something went wrong. Please try again later.'
};
function getErrorMessage(error: ApiError): string {
return ERROR_MESSAGES[error.error] || ERROR_MESSAGES.DEFAULT;
}
Logging Errors
Log errors for debugging:
function logError(error: ApiError, context?: Record<string, unknown>) {
console.error('API Error:', {
code: error.error,
message: error.message,
status: error.statusCode,
details: error.details,
...context
});
// Send to error tracking service
if (process.env.NODE_ENV === 'production') {
errorTracker.capture(error, context);
}
}
// Usage
const result = await sdk.eshop.checkout({ ... });
if (!result.ok) {
logError(result.val, {
orderId: order.id,
userId: currentUser.id
});
}
Tip
Always provide clear, actionable error messages to users. Technical details should be logged but not displayed.
Testing Error Scenarios
// Test error handling
describe('Product Page', () => {
it('shows error message when product not found', async () => {
// Mock API to return error
vi.spyOn(sdk.eshop, 'getProduct').mockResolvedValue({
ok: false,
val: {
error: 'NOT_FOUND',
message: 'Product not found',
statusCode: 404
}
});
render(<ProductPage id="invalid" />);
await waitFor(() => {
expect(screen.getByText(/not found/i)).toBeInTheDocument();
});
});
});