@deliverart/sdk-js-error-handler
Error handling plugin for the DeliverArt JavaScript SDK. Automatically handles API errors and transforms them into typed exceptions.
Installation
npm install @deliverart/sdk-js-error-handler @deliverart/sdk-js-core
# or
pnpm add @deliverart/sdk-js-error-handler @deliverart/sdk-js-core
# or
yarn add @deliverart/sdk-js-error-handler @deliverart/sdk-js-core
Exported Types
ErrorHandlerPlugin
class ErrorHandlerPlugin implements ApiClientPlugin<{}>
Plugin that intercepts API responses and transforms error responses into typed exceptions.
Constructor:
constructor()- No configuration required
ApiError
class ApiError extends Error {
readonly name = 'ApiError'
readonly code?: string
readonly request?: RequestInit
readonly response?: Response
readonly status?: number
readonly data?: ApiErrorData
}
Main error class for API errors.
Properties:
name: 'ApiError'- Error namecode?: string- Error code from the APIrequest?: RequestInit- Original request initresponse?: Response- HTTP response objectstatus?: number- HTTP status codedata?: ApiErrorData- Typed error data
ApiErrorData
type ApiErrorData =
| {
type: 'VALIDATION_ERROR'
value: ValidationErrorData
}
| {
type: 'UNKNOWN_ERROR'
value: unknown
}
Discriminated union type for different error types.
VALIDATION_ERROR - Returned when API responds with 422 status (validation failed)
value: ValidationErrorData- Contains validation violations
UNKNOWN_ERROR - Returned for all other error responses
value: unknown- Contains the raw error response
ValidationErrorData
interface ValidationErrorData {
status: number
violations: Array<{
propertyPath: string
message: string
code: string | null
}>
detail: string
type: string
title: string
}
Structured validation error data from the API.
Properties:
status: number- HTTP status code (422)violations: Array- List of validation violationspropertyPath: string- Field path that failed validation (e.g., "email", "address.city")message: string- Human-readable error messagecode: string | null- Validation error code
detail: string- Detailed error descriptiontype: string- Error type identifiertitle: string- Error title
FormState
type FormState<T> =
| {
success: true
data: T
}
| {
success: false
error: ApiErrorData
}
Utility type for handling form submission states (useful for React Server Actions).
FormStateError
class FormStateError extends Error {
constructor(public readonly error: ApiErrorData)
}
Error wrapper for form state errors.
Usage
Basic Setup
import { createApiClient } from '@deliverart/sdk-js-core';
import { ErrorHandlerPlugin } from '@deliverart/sdk-js-error-handler';
const client = createApiClient({
baseUrl: 'https://api.deliverart.com'
})
.addPlugin(new ErrorHandlerPlugin());
Handling Validation Errors
import { ApiError } from '@deliverart/sdk-js-error-handler';
import { CreateUser } from './requests';
try {
await client.call(new CreateUser({
email: 'invalid-email',
name: ''
}));
} catch (error) {
if (error instanceof ApiError && error.data?.type === 'VALIDATION_ERROR') {
const violations = error.data.value.violations;
violations.forEach(violation => {
console.error(`${violation.propertyPath}: ${violation.message}`);
});
// Output:
// email: This value is not a valid email address.
// name: This value should not be blank.
}
}
Displaying Validation Errors in Forms
React Example
import { useState } from 'react';
import { ApiError } from '@deliverart/sdk-js-error-handler';
function CreateUserForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrors({});
try {
await client.call(new CreateUser(formData));
// Success!
} catch (error) {
if (error instanceof ApiError && error.data?.type === 'VALIDATION_ERROR') {
const newErrors: Record<string, string> = {};
error.data.value.violations.forEach(violation => {
newErrors[violation.propertyPath] = violation.message;
});
setErrors(newErrors);
}
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" />
{errors.email && <span className="error">{errors.email}</span>}
<input name="name" />
{errors.name && <span className="error">{errors.name}</span>}
<button type="submit">Create User</button>
</form>
);
}
Handling Different HTTP Status Codes
import { ApiError } from '@deliverart/sdk-js-error-handler';
try {
await client.call(new GetUser('123'));
} catch (error) {
if (error instanceof ApiError) {
switch (error.status) {
case 400:
console.error('Bad Request:', error.message);
break;
case 401:
console.error('Unauthorized - please login');
window.location.href = '/login';
break;
case 403:
console.error('Forbidden - insufficient permissions');
break;
case 404:
console.error('User not found');
break;
case 422:
// Validation error - handled above
break;
case 500:
console.error('Server error');
break;
default:
console.error('Unknown error:', error);
}
}
}
Using FormState with React Server Actions
FormState is particularly useful for Next.js Server Actions:
// app/actions/create-user.ts
'use server';
import { throwableToFormState, FormState } from '@deliverart/sdk-js-error-handler';
import { sdk } from '@/lib/sdk';
import { CreateUser } from '@deliverart/sdk-js-user';
export async function createUserAction(
formData: FormData
): Promise<FormState<{ id: string }>> {
return throwableToFormState(async () => {
const result = await sdk.call(new CreateUser({
name: formData.get('name') as string,
email: formData.get('email') as string,
}));
return { id: result.id };
});
}
// app/components/CreateUserForm.tsx
'use client';
import { useFormState } from 'react-dom';
import { createUserAction } from '../actions/create-user';
export function CreateUserForm() {
const [state, formAction] = useFormState(createUserAction, null);
return (
<form action={formAction}>
<input name="name" required />
{state?.success === false &&
state.error.type === 'VALIDATION_ERROR' &&
state.error.value.violations
.filter(v => v.propertyPath === 'name')
.map(v => <span key={v.code} className="error">{v.message}</span>)
}
<input name="email" type="email" required />
{state?.success === false &&
state.error.type === 'VALIDATION_ERROR' &&
state.error.value.violations
.filter(v => v.propertyPath === 'email')
.map(v => <span key={v.code} className="error">{v.message}</span>)
}
<button type="submit">Create User</button>
{state?.success === true && (
<p className="success">User created with ID: {state.data.id}</p>
)}
</form>
);
}
Global Error Handler
Set up a global error handler for all API errors:
// lib/error-handler.ts
import { ApiError } from '@deliverart/sdk-js-error-handler';
export function handleApiError(error: unknown) {
if (!(error instanceof ApiError)) {
console.error('Non-API error:', error);
return;
}
// Log to error tracking service (e.g., Sentry)
console.error('API Error:', {
status: error.status,
code: error.code,
message: error.message,
data: error.data,
});
// Show user-friendly message
if (error.status === 401) {
alert('Please login to continue');
window.location.href = '/login';
} else if (error.status === 403) {
alert('You do not have permission to perform this action');
} else if (error.status === 404) {
alert('The requested resource was not found');
} else if (error.data?.type === 'VALIDATION_ERROR') {
const firstViolation = error.data.value.violations[0];
alert(`Validation error: ${firstViolation.message}`);
} else {
alert('An unexpected error occurred. Please try again.');
}
}
// Usage in your app
try {
await client.call(request);
} catch (error) {
handleApiError(error);
}
Type Guards
Create type guards for cleaner error handling:
import { ApiError, ApiErrorData } from '@deliverart/sdk-js-error-handler';
function isValidationError(error: unknown): error is ApiError & {
data: { type: 'VALIDATION_ERROR', value: ValidationErrorData }
} {
return error instanceof ApiError &&
error.data?.type === 'VALIDATION_ERROR';
}
function isUnauthorizedError(error: unknown): error is ApiError {
return error instanceof ApiError && error.status === 401;
}
function isNotFoundError(error: unknown): error is ApiError {
return error instanceof ApiError && error.status === 404;
}
// Usage
try {
await client.call(request);
} catch (error) {
if (isValidationError(error)) {
// TypeScript knows error.data.value is ValidationErrorData
error.data.value.violations.forEach(v => {
console.log(v.propertyPath, v.message);
});
} else if (isUnauthorizedError(error)) {
redirectToLogin();
} else if (isNotFoundError(error)) {
show404Page();
}
}
Converting FormState Errors Back to Exceptions
import { formStateErrorToThrowable, FormState } from '@deliverart/sdk-js-error-handler';
const result: FormState<User> = await createUserAction(formData);
if (!result.success) {
// Convert FormState error back to throwable error
throw formStateErrorToThrowable(result.error);
}
// TypeScript knows result.data is User
console.log(result.data.id);
How It Works
The plugin adds a response middleware that:
- Checks if the response is not OK (
!response.ok) - If status is 422, attempts to parse the response as validation error
- If parsing succeeds, throws
ApiErrorwithVALIDATION_ERRORtype - For all other error responses, throws
ApiErrorwithUNKNOWN_ERRORtype - Includes the original request, response, and parsed body in the error
Best Practices
1. Centralized Error Handling
Create a utility function for consistent error handling across your app:
export function getErrorMessage(error: unknown): string {
if (error instanceof ApiError) {
if (error.data?.type === 'VALIDATION_ERROR') {
const firstViolation = error.data.value.violations[0];
return firstViolation?.message ?? 'Validation failed';
}
return error.message;
}
if (error instanceof Error) {
return error.message;
}
return 'An unexpected error occurred';
}
2. Field-Level Error Mapping
Create a helper to map violations to form fields:
import { ValidationErrorData } from '@deliverart/sdk-js-error-handler';
export function violationsToFieldErrors(
violations: ValidationErrorData['violations']
): Record<string, string> {
return violations.reduce((acc, violation) => {
acc[violation.propertyPath] = violation.message;
return acc;
}, {} as Record<string, string>);
}
// Usage
if (error.data?.type === 'VALIDATION_ERROR') {
const fieldErrors = violationsToFieldErrors(error.data.value.violations);
setErrors(fieldErrors);
}
3. Toast Notifications
Integrate with toast notification libraries:
import { toast } from 'sonner';
import { ApiError } from '@deliverart/sdk-js-error-handler';
try {
await client.call(request);
toast.success('Operation completed successfully');
} catch (error) {
if (error instanceof ApiError && error.data?.type === 'VALIDATION_ERROR') {
error.data.value.violations.forEach(v => {
toast.error(v.message);
});
} else {
toast.error('An error occurred');
}
}
License
MIT
Safe diagnostics
ApiError keeps its original request, response, data, code, status and message readable for typed consumers and form-state conversion. Transport objects, raw bodies and status/code fields are non-enumerable. Default Node inspection, string formatting and JSON serialization expose only an allowlisted diagnostic: stable classification, HTTP status/code, method and a pathname without query, fragment, origin or user information. The plugin captures that pathname from the actual request context. Validation field messages remain available programmatically and are not included in these diagnostics.
Do not log raw error.request, error.response, error.data, custom caller messages or arbitrary stack/exception serializers. This SDK behavior does not sanitize Next.js access logs, OAuth callback URLs logged elsewhere, or independent telemetry request contexts.
The focused built-artifact tests exercise ESM and CommonJS diagnostics, secret omission, actual response middleware and form-state compatibility. Build this package and its workspace dependencies first, then run pnpm --filter @deliverart/sdk-js-error-handler test. No test runner dependency is added: tests use Node's built-in runner.