Real-World Examples
This section shows patterns from production integrations you can copy and adapt. Each example explains the problem, the pattern, and why it works.
Pattern 1: SAP Out of Stock Detection
The Problem
SAP Commerce Cloud returns HTTP 200 success even when adding an out-of-stock product. The error is hidden in the response payload under cartModifications[].statusCode.
Without checking the payload, your middleware returns success. Frontend shows "Item added to cart" but the cart is empty. User clicks multiple times, gets confused.
The Pattern
Check vendor response payloads for error indicators, even on success:
export const addCartLineItem = async (context, args) => {
const { api } = await context.getApiClient();
const { data } = await api.doAddOrgCartEntries({
cartId: args.cartId,
orderEntryList: {
orderEntries: [{ product: { code: args.sku }, quantity: args.quantity }],
},
});
// Check payload for error indicator
if (data?.cartModifications?.find(m => m.statusCode === 'noStock')) {
throw context.createHttpError({
statusCode: 409, // Conflict
message: 'Product is out of stock',
data: { errors: [{ type: 'InsufficientStockError' }] },
});
}
// Success case
const { data: cart } = await api.getCart({ cartId: args.cartId });
return normalizeCart(cart);
};
Why It Works
- ✅ 409 Conflict clearly indicates business constraint (not server error)
- ✅ Structured error data (
InsufficientStockError) lets frontend show specific message - ✅ No try/catch needed — checking response, not catching exception
Apply this pattern whenever your vendor returns success codes with error indicators. Common in: SAP, some Commerce Tools scenarios, custom internal APIs.
Pattern 2: Password Change Error Mapping
The Problem
External API returns 401 Unauthorized when the current password is wrong. That's technically correct, but confusing for users. Frontend should show "Current password is incorrect" (field-level error), not "Unauthorized".
The Pattern
Catch the error, inspect it, transform into user-friendly format:
export const changePassword = async (context, args) => {
const { api } = await context.getApiClient();
try {
await api.updatePassword({
userId: args.userId,
oldPassword: args.currentPassword,
newPassword: args.newPassword,
});
return { success: true };
} catch (error) {
// Error already normalized by adapter
const normalized = HttpError.isHttpError(error) ? error : new HttpError(500, 'Unknown error');
// Transform technical error into field-specific error
if (normalized.statusCode === 401) {
throw context.createHttpError({
statusCode: 400, // Bad Request (client error)
message: 'Current password is incorrect',
data: { field: 'currentPassword' }, // Frontend can highlight this field
cause: error, // Preserve for debugging
});
}
// Other errors pass through
throw normalized;
}
};
Why It Works
- ✅ User-friendly message instead of "Unauthorized"
- ✅ Field-level error data lets frontend highlight the
currentPasswordfield - ✅ Original error preserved in
causefor debugging - ✅ Other errors pass through (don't hide unexpected errors)
Apply this pattern for form submissions where you need field-level error feedback: login, registration, password changes, profile updates.
Pattern 3: External API Normalization
The Problem
You're calling an external address validation service (not part of your commerce platform). If it throws, the error isn't normalized. Circuit breaker gets confused, status codes are wrong.
The Pattern
Configure HTTP client with error adapter once:
// In index.server.ts - one-time setup
import { axiosAdapter } from "@alokai/middleware-axios-error-adapter";
const validatorClient = axiosAdapter.withErrorNormalizer(
axios.create({ baseURL: 'https://validator.example.com' }),
{ extractMessage: (payload) => payload?.error },
);
// In endpoint - clean code
export const validateAddress = async (context, args) => {
// No try/catch needed - adapter handles normalization
const { data } = await validatorClient.post('/check', {
address: args.address,
});
return { isValid: data.valid };
};
Why It Works
- ✅ Status codes extracted properly (404, 503, etc.)
- ✅ One-time configuration in setup file
- ✅ No repetitive try/catch blocks in endpoints
- ✅ Circuit breaker treats 4xx as business errors (doesn't open on user typos)
Apply this pattern for any external service: payment gateways, shipping calculators, fraud detection, recommendation engines, etc.
Pattern 4: GraphQL Error Handling
The Problem
GraphQL APIs return HTTP 200 even with errors. Errors are in the response body under errors array. Without normalization, you get inconsistent status codes.
The Pattern
Configure Apollo Client with error adapter:
// In apolloClient.ts - one-time setup
import { apolloAdapter } from "@alokai/middleware-apollo-error-adapter";
export const apolloClientFactory = (options) => {
const client = new ApolloClient({
cache: new InMemoryCache(),
...options,
});
return apolloAdapter.withErrorNormalizer(client, {
extractMessage: (errors) => errors?.[0]?.message,
});
};
// In endpoint - clean code
export const getProducts = async (context, args) => {
// No try/catch needed - adapter handles normalization
return await context.client.query({
query: GET_PRODUCTS_QUERY,
variables: { categoryId: args.categoryId },
});
};
Why It Works
- ✅ Maps GraphQL error codes to HTTP status:
UNAUTHENTICATED→ 401,FORBIDDEN→ 403, etc. - ✅ Distinguishes business errors (422) from infrastructure errors (502)
- ✅ One-time configuration in client factory
- ✅ No repetitive error handling in endpoints
Apply this pattern for all GraphQL-based integrations. The normalizer handles error extraction from different GraphQL clients automatically.
Pattern 5: Error Categorization Helper
The Problem
Frontend needs to show different UI for different SAP error types:
InsufficientStockError→ Show "Out of stock" badgeUnknownIdentifierError→ Show "Product not found" message- Other errors → Show generic "Error occurred"
The error might come from error.data (manually thrown) or error.cause.response.data (from Axios).
The Pattern
Create a helper that checks both locations:
import { HttpError } from '@alokai/connect/middleware';
// Check both possible locations for error type
const isSpecificSAPError = (error: HttpError, errorType: string): boolean => {
return (
// Manually thrown errors
error.data?.errors?.[0]?.type === errorType ||
// Axios errors (preserved in cause)
(error.cause as any)?.response?.data?.errors?.[0]?.type === errorType
);
};
// Categorize for frontend
export const categorizeQuickOrderError = (error: unknown): ErrorCategory => {
if (!HttpError.isHttpError(error)) {
return 'unknownError';
}
if (isSpecificSAPError(error, 'InsufficientStockError')) {
return 'noQuantity'; // Frontend knows how to handle this
}
if (isSpecificSAPError(error, 'UnknownIdentifierError')) {
return 'notFound'; // Frontend knows how to handle this
}
return 'unknownError'; // Fallback
};
Why It Works
- ✅ Single helper handles both error locations
- ✅ Type-safe with
HttpError.isHttpError()check - ✅ Fallback to
unknownErrorprevents crashes - ✅ Frontend gets consistent categories regardless of error origin
Apply this pattern when you have multiple error types from the same vendor and need to categorize them for frontend consumption.
Pattern 6: Validation with Schema Library
The Problem
You need to validate user input (e.g., form data) before processing. Manual validation is tedious and error-prone. You want structured error messages with field paths for frontend to display.
The Pattern
Use a schema validation library like zod or valibot:
import { z } from 'zod';
export const createUser = async (context, args) => {
const schema = z.object({
email: z.email(),
username: z.string().min(3).max(20),
age: z.number().min(18),
});
// Validation errors are auto-normalized to ValidationError with 400 status
const validated = schema.parse(args);
// Use validated data
const { api } = await context.getApiClient();
return api.createUser(validated);
};
Response on Validation Error
{
"name": "ValidationError",
"message": "Validation failed",
"data": {
"issues": [
{ "message": "Invalid email", "path": ["email"] },
{ "message": "String must contain at least 3 character(s)", "path": ["username"] }
]
}
}
Apply this pattern for any user input validation: form submissions, query parameters, request bodies. Works with zod, valibot, arktype, and 20+ other Standard Schema libraries.
Common Patterns Summary
| Pattern | When to Use | Key Tool |
|---|---|---|
| Payload check | Vendor returns 200 with error indicator | Check response, context.createHttpError() |
| Error mapping | Transform technical errors into user-friendly | try/catch + context.createHttpError() |
| External API | Calling third-party services | Configure error adapter in client setup |
| GraphQL API | GraphQL-based integrations | Configure error adapter in client factory |
| Error categorization | Multiple vendor error types | Helper function + HttpError.isHttpError() |
| Input validation | Validating user/request data | Schema library + auto-normalization |
Next Steps
Now that you've seen the patterns, you're ready to implement error handling in your integration.
Start small: Pick one endpoint, apply the appropriate pattern, test it. Then expand to other endpoints.
Need help deciding which pattern to use? Go back to When to Use try/catch for a decision guide.