Error Handling Patterns
How errors flow through the DTX Portal — from backend API responses to user-facing feedback.
Error Flow Architecture
API Error Handling
The parseApiResponse Pipeline
Every API call goes through parseApiResponse<T>() which:
- Checks HTTP status code
- Parses JSON response body
- Unwraps
StandardApiResponse<T>wrapper - Converts
snake_casekeys tocamelCase - Throws
ApiErroron non-2xx status
// Service call pattern
const response = await fetchWithAuth('/api/v1/pipelines', { method: 'POST', body });
const data = await parseApiResponse<Pipeline>(response);
// If status >= 400, parseApiResponse throws ApiError
Error Types by Status Code
| Status | Type | Handling Pattern |
|---|---|---|
| 401 | Unauthorized | Keycloak token refresh triggers automatically. If refresh fails, user is redirected to login page. |
| 403 | Forbidden | Toast: "You don't have permission to perform this action." No retry. |
| 404 | Not Found | Toast or inline message depending on context. Navigate away if resource deleted. |
| 409 | Conflict | Toast: "Resource was modified by another user." Prompt to reload. |
| 422 | Validation | Field-level errors mapped to form fields via setError(). See Form Validation below. |
| 429 | Rate Limited | Toast: "Too many requests." Automatic retry after delay (React Query retry: 1). |
| 500 | Server Error | Toast: "Something went wrong. Please try again." Log to console for debugging. |
| Network | Timeout/Offline | Toast: "Unable to reach the server. Check your connection." |
Form Validation Errors (422)
Backend returns 422 with field-level error details. These map directly to React Hook Form fields.
Backend Response Format
{
"status": "error",
"message": "Validation failed",
"errors": [
{ "field": "name", "message": "Name must be between 3 and 100 characters" },
{ "field": "config.kafkaTopic", "message": "Topic does not exist" }
]
}
Frontend Mapping Pattern
// In a mutation's onError callback
onError: (error) => {
if (isValidationError(error)) {
// Map each backend field error to the form
error.fieldErrors.forEach(({ field, message }) => {
form.setError(field, { type: 'server', message });
});
} else {
// Generic error — show toast
toast.error(error.message || 'Something went wrong');
}
}
Multi-Step Wizard Validation
For multi-step forms (e.g., SDG generator wizard), validation errors may reference fields on a different step than the current one:
onErrorreceives 422 with field errors- Map errors to form via
setError() - Navigate user to the step containing the errored field
- Field shows inline error message
Component Error Boundaries
React error boundaries catch rendering errors and display fallback UI instead of a white screen.
Global Error Boundary
Wraps all routes — catches unhandled render errors anywhere in the app.
// Used in router configuration
<ErrorBoundary fallback={<ErrorFallbackPage />}>
<Outlet />
</ErrorBoundary>
Feature-Level Error Boundaries
Wrap individual features so one broken component doesn't take down the whole page:
<ErrorBoundary fallback={<ErrorCard message="Failed to load pipeline list" />}>
<PipelineList />
</ErrorBoundary>
Error boundaries only catch render errors (exceptions thrown during rendering). They do NOT catch errors in event handlers, async code, or API calls — those need explicit try/catch or React Query's onError.
Toast Notifications (Sonner)
The app uses Sonner for all user-facing notifications.
Patterns
| Scenario | Toast Type | Example |
|---|---|---|
| Successful mutation | toast.success() | "Pipeline created successfully" |
| API error (non-422) | toast.error() | "Failed to compile pipeline" |
| Permission denied | toast.error() | "You don't have permission" |
| Network failure | toast.error() | "Unable to reach the server" |
| Warning (non-blocking) | toast.warning() | "Unsaved changes will be lost" |
| Background action complete | toast.info() | "Compilation finished" |
Usage in Mutations
const mutation = useMutation({
mutationFn: (data) => pipelineService.create(data),
onSuccess: () => {
toast.success('Pipeline created successfully');
queryClient.invalidateQueries({ queryKey: ['pipelines'] });
},
onError: (error) => {
toast.error(error.message || 'Failed to create pipeline');
},
});
Designer Error Handling
The pipeline and workflow designers have specialized error handling because of their complexity.
Auto-Save Failures
usePipelineStore persists to IndexedDB. If a write fails:
- Error is logged to console (no user-facing toast — auto-save is silent)
- In-memory state remains intact
- Next auto-save attempt retries the write
- If IndexedDB is unavailable (private browsing, storage full), state lives only in memory
Compilation Errors
When pipeline compilation fails (API returns error):
useCompilationProgresspolling detects failure- Pipeline status set to
FAILED - Error message displayed in the designer status bar
- User can fix issues and re-trigger compilation
Recovery
usePipelineRecovery() checks IndexedDB on page load:
- If unsaved state exists from a crash, prompt user to recover or discard
- Recovery restores the full canvas state (nodes, edges, configurations)
- Discard clears IndexedDB entry
Common Pitfalls
Double Submission
Problem: User clicks "Save" twice quickly, creating duplicate resources.
Solution: Disable the submit button while mutation is pending:
<Button disabled={mutation.isPending} onClick={handleSubmit}>
{mutation.isPending ? 'Saving...' : 'Save'}
</Button>
Stale Cache After Mutation
Problem: After creating/updating a resource, the list still shows old data.
Solution: Invalidate related queries in onSuccess:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['pipelines'] });
}
Race Conditions
Problem: Multiple concurrent requests for the same resource return in unexpected order.
Solution: React Query handles this — the most recent request's data wins. For mutations, disable further actions while one is in flight (mutation.isPending).
Silent Failures
Problem: An API call fails but the user sees no feedback.
Solution: Always handle onError in mutations. For queries, React Query shows error state via isError / error fields — render an error message in the component.