Table of Contents
When consuming external APIs, error responses come in all shapes and sizes. Some vendors nest errors deeply (error.response.data.error.message), others put them at the root (error.message), and some just throw generic HTTP status text.
If you’re not defensive about extracting these messages, your users will see unhelpful errors like “undefined” or “Cannot read property ‘message’ of undefined” instead of the actual problem.
The Pattern: Fallback Chains with Existence Checks
Build a chain of fallback attempts, each guarded by existence checks:
// Frontend API call (Vue.js, React, plain JS)
try {
const response = await axios.get('/api/products/123')
this.product = response.data
} catch (error) {
const message = (
error.response?.data?.error?.message || // Try nested first
error.response?.data?.message || // Then less nested
error.message || // Generic error message
'Failed to load product data' // Hardcoded fallback
)
this.showError(message)
}
The same pattern works server-side in PHP/Laravel:
use Illuminate\Support\Facades\Http;
try {
$response = Http::get('https://api.vendor.com/products/123');
return $response->json();
} catch (\Exception $e) {
$message = $e->response['error']['message']
?? $e->response['message']
?? $e->getMessage()
?? 'Failed to fetch product from external API';
Log::error('External API error', ['message' => $message]);
throw new \RuntimeException($message);
}
Why This Matters
This pattern:
- Prevents runtime errors: Each level is optional-chained (
?.or??), so missing properties don’t crash - Provides useful fallbacks: Users see the most specific error available, never “undefined”
- Works across vendors: Different API error formats are handled gracefully
- Debuggable: Hardcoded fallback tells you “we got an error but couldn’t extract a message”
Common API Error Formats
Here are the patterns you’ll encounter:
// Format 1: Deeply nested (Stripe-style)
{
"error": {
"type": "invalid_request_error",
"message": "No such customer: cus_xxxxx"
}
}
// Format 2: Root-level message (Laravel-style)
{
"message": "The given data was invalid.",
"errors": { ... }
}
// Format 3: HTTP status text only
// (no JSON body, just 500 Internal Server Error)
Your fallback chain handles all three:
- Try
error.response.data.error.messageβ catches Format 1 - Try
error.response.data.messageβ catches Format 2 - Try
error.messageβ catches Format 3 (Axios wraps HTTP status text here) - Use hardcoded fallback β catches unexpected formats
Laravel HTTP Client Version
Laravel’s HTTP client throws different exceptions, so adjust your chain:
use Illuminate\Http\Client\RequestException;
try {
$response = Http::timeout(10)->get('https://api.vendor.com/data');
return $response->throw()->json();
} catch (RequestException $e) {
// Laravel wraps the response
$message = data_get($e->response, 'error.message')
?? data_get($e->response, 'message')
?? $e->getMessage()
?? 'API request failed';
throw new \RuntimeException($message);
}
Using data_get() is cleaner than ?? chains for deeply nested arrays.
When to Use This
- Any external API integration (payment gateways, shipping APIs, third-party services)
- Frontend API calls where you display errors to users
- Background jobs that need to log meaningful error messages
- Webhook handlers that receive errors from external systems
Remember: Never assume error responses will match the API docs. Build fallback chains so your users (and logs) always see a meaningful message, even when the API returns something unexpected.
Leave a Reply