Skip to main content

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:

  1. Checks HTTP status code
  2. Parses JSON response body
  3. Unwraps StandardApiResponse<T> wrapper
  4. Converts snake_case keys to camelCase
  5. Throws ApiError on 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

StatusTypeHandling Pattern
401UnauthorizedKeycloak token refresh triggers automatically. If refresh fails, user is redirected to login page.
403ForbiddenToast: "You don't have permission to perform this action." No retry.
404Not FoundToast or inline message depending on context. Navigate away if resource deleted.
409ConflictToast: "Resource was modified by another user." Prompt to reload.
422ValidationField-level errors mapped to form fields via setError(). See Form Validation below.
429Rate LimitedToast: "Too many requests." Automatic retry after delay (React Query retry: 1).
500Server ErrorToast: "Something went wrong. Please try again." Log to console for debugging.
NetworkTimeout/OfflineToast: "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:

  1. onError receives 422 with field errors
  2. Map errors to form via setError()
  3. Navigate user to the step containing the errored field
  4. 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>
tip

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

ScenarioToast TypeExample
Successful mutationtoast.success()"Pipeline created successfully"
API error (non-422)toast.error()"Failed to compile pipeline"
Permission deniedtoast.error()"You don't have permission"
Network failuretoast.error()"Unable to reach the server"
Warning (non-blocking)toast.warning()"Unsaved changes will be lost"
Background action completetoast.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:

  1. Error is logged to console (no user-facing toast — auto-save is silent)
  2. In-memory state remains intact
  3. Next auto-save attempt retries the write
  4. If IndexedDB is unavailable (private browsing, storage full), state lives only in memory

Compilation Errors

When pipeline compilation fails (API returns error):

  1. useCompilationProgress polling detects failure
  2. Pipeline status set to FAILED
  3. Error message displayed in the designer status bar
  4. User can fix issues and re-trigger compilation

Recovery

usePipelineRecovery() checks IndexedDB on page load:

  1. If unsaved state exists from a crash, prompt user to recover or discard
  2. Recovery restores the full canvas state (nodes, edges, configurations)
  3. 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.