When to Use try/catch
This is the most important section for writing clean middleware code. Understanding when to use (and skip) try/catch will save you from writing unnecessary code.
The Golden Rule
Only wrap external API calls in try/catch.
If you're calling methods from your integration's api object, they already normalize errors. No try/catch needed.
Scenario 1: Integration Methods (Skip try/catch)
When this applies
You're calling methods like:
api.getCart()api.addCartEntry()api.getProduct()- Any method from
context.getApiClient()
Why skip try/catch?
Integration methods already throw normalized HttpError instances with proper status codes. Wrapping them in try/catch is redundant.
Example
export const updateCart = async (context, args) => {
const { api } = await context.getApiClient();
// ✅ No try/catch needed
const { data: cart } = await api.getCart({ cartId: args.cartId });
await api.addCartEntry({ cartId: args.cartId, sku: args.sku });
return normalizeCart(cart);
};
Think of integration methods as "already safe" — they handle error normalization for you.
Scenario 2: External APIs (Usually Skip try/catch)
When this applies
You're calling:
- Third-party APIs with HTTP clients
- External services (address validation, payment gateways, etc.)
- Any API not part of your integration's
apiobject
Why usually skip try/catch?
If your HTTP client is configured with an error adapter (in index.server.ts or client factory), errors are automatically normalized. No try/catch needed.
Example
// In index.server.ts - configure once with error adapter
const validatorClient = adapter.withErrorNormalizer(
httpClient,
{ extractMessage: (payload) => payload?.error },
);
// In your endpoint - no try/catch needed
export const validateAddress = async (context, args) => {
// Errors automatically normalized by adapter
const { data } = await validatorClient.post('/check', {
address: args.address,
});
return { isValid: data.valid };
};
When to add try/catch
Only if you need custom error transformation:
export const validateAddress = async (context, args) => {
try {
const { data } = await validatorClient.post('/check', {
address: args.address,
});
return { isValid: data.valid };
} catch (error) {
// Transform adapter-normalized error for specific UX
const normalized = HttpError.isHttpError(error) ? error : new HttpError(500, 'Unknown error');
if (normalized.statusCode === 422) {
throw context.createHttpError({
statusCode: 400,
message: 'Please provide a complete address',
data: { field: 'address' },
cause: error,
});
}
throw normalized;
}
};
Best practice: Configure adapters once in setup files. Your endpoint methods stay clean without try/catch blocks.
Scenario 3: Custom Error Mapping (Use try/catch)
When this applies
You need to:
- Transform technical errors into user-friendly messages
- Map vendor error codes to custom messages
- Add frontend-specific error data
Why use try/catch?
You're catching the error, inspecting it, and throwing a new, more specific error.
Example: Password change
Technical error: 401 Unauthorized
User-friendly error: "Current password is incorrect"
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) {
const normalized = normalizeAxiosError(error);
// Map 401 to user-friendly message
if (normalized.statusCode === 401) {
throw context.createHttpError({
statusCode: 400,
message: 'Current password is incorrect',
data: { field: 'currentPassword' },
cause: error,
});
}
// Re-throw other errors as-is
throw normalized;
}
};
The key: You're adding value by making the error more specific for your frontend.
Scenario 4: Vendor Payload Errors (No try/catch)
When this applies
Vendor returns HTTP 200 with error indicators in the response body (like SAP's statusCode: 'noStock').
Why skip try/catch?
There's no exception thrown — the API call succeeded. You check the response and throw manually.
Example
export const addToCart = async (context, args) => {
const { api } = await context.getApiClient();
const { data } = await api.addCartEntry(args);
// Check response payload (no try/catch needed)
if (data?.modifications?.find(m => m.statusCode === 'noStock')) {
throw context.createHttpError({
statusCode: 409,
message: 'Product is out of stock',
data: { errors: [{ type: 'InsufficientStockError' }] },
});
}
return normalizeCart(data);
};
Decision Flowchart
Use this to decide whether you need try/catch:
Are you calling an integration method?
│
├─ YES → ❌ Skip try/catch
│ (Already normalized)
│
└─ NO → Are you calling an external API?
│
├─ YES → ✅ Use try/catch
│ (Need to normalize)
│
└─ NO → Do you need custom error mapping?
│
├─ YES → ✅ Use try/catch
│ (Transform the error)
│
└─ NO → ❌ Skip try/catch
(Let errors propagate)
Common Mistakes
❌ Wrapping integration methods
// DON'T
try {
const cart = await api.getCart({ cartId });
} catch (error) {
throw normalizeAxiosError(error); // Redundant!
}
Problem: You're normalizing an already-normalized error.
Fix: Remove the try/catch. Let api.getCart() throw directly.
❌ Forgetting external APIs
// DON'T
const { data } = await axios.get(externalUrl); // Missing try/catch!
Problem: If Axios throws, the error isn't normalized. Wrong status code in logs, circuit breaker confusion.
Fix: Wrap in try/catch and use normalizeAxiosError().
Quick Summary
| Scenario | try/catch? | Tool to Use |
|---|---|---|
Integration method (api.getCart()) | ❌ No | Let it throw |
| External API (Axios/Fetch) | ✅ Yes | normalizeAxiosError() |
| Custom error mapping | ✅ Yes | context.createHttpError() |
| Vendor payload errors (HTTP 200) | ❌ No | Check response, throw manually |