Error Handling
PreflightAPI uses standard HTTP status codes and returns JSON error responses. Errors originate from two sources — the APIM gateway (auth, rate limits, quotas, tier-gating) and the backend API (validation, not found, server errors) — each with a distinct response format.
Gateway Errors (401, 429, 403 Quota)
These errors are generated by the Azure API Management gateway before the request reaches the backend. They use a simple statusCode + message format:
{
"statusCode": 429,
"message": "Rate limit is exceeded. Try again in 52 seconds."
}| Status | Cause | Example Message |
|---|---|---|
401 | Missing or invalid subscription key | "Access denied due to invalid subscription key. Make sure to provide a valid key for an active subscription." |
429 | Rate limit exceeded | "Rate limit is exceeded. Try again in 52 seconds." |
403 | Monthly quota exceeded | "Out of call volume quota. Quota will be replenished in 06:23:15." |
Tier-Gating Errors (403)
When your subscription plan does not include access to the requested endpoint, the APIM gateway returns a 403 Forbidden response with a different format — an error string field instead of statusCode:
{
"error": "This endpoint is not available on the Free tier. Please upgrade to Starter or Professional."
}See the endpoint access table for which endpoints are available on each plan.
You can distinguish the two types of 403 by checking the response body: quota exceeded has { statusCode, message }, while tier-gating has { error }.
Backend API Errors (400, 404, 409, 500, 503)
Errors generated by the API backend use a rich, structured format with a machine-readable code for programmatic handling and a traceId for support:
{
"code": "METAR_NOT_FOUND",
"message": "No current METAR available for station 'KXYZ'",
"timestamp": "2026-01-15T18:56:00Z",
"traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"path": "/api/v1/metars/KXYZ"
}| Field | Type | Description | Always Present |
|---|---|---|---|
code | string | Machine-readable error code (e.g., METAR_NOT_FOUND) | Yes |
message | string | Human-readable description of what went wrong | Yes |
timestamp | string | ISO 8601 UTC timestamp of when the error occurred | Yes |
traceId | string? | Correlation ID — include this when contacting support | Usually |
path | string? | The request path that generated the error | Usually |
details | string? | Additional context (development environments only) | No |
validationErrors | object? | Field-level errors (only for 400 validation failures) | No |
HTTP Status Codes
| Code | Name | Source | Description |
|---|---|---|---|
400 | Bad Request | Backend | Invalid query parameters, missing required fields, or invalid values. |
401 | Unauthorized | APIM Gateway | Missing or invalid API key. Check the Ocp-Apim-Subscription-Key header. |
403 | Forbidden | APIM Gateway | Endpoint not available on your plan (tier-gating) or monthly quota exceeded. |
404 | Not Found | Backend | The requested resource does not exist (e.g., unknown ICAO code, no METAR available). |
409 | Conflict | Backend | The request conflicts with the current state (e.g., duplicate resource). |
429 | Too Many Requests | APIM Gateway | Rate limit exceeded. Includes a Retry-After header. See the rate limits page for details. |
500 | Internal Server Error | Backend | An unexpected error occurred. Include the traceId when contacting support. |
503 | Service Unavailable | Backend | An external data source (NOAA, FAA) is temporarily unavailable. Retry after a short delay. |
Error Codes Reference
The code field in backend error responses contains one of these machine-readable values. Use these to handle specific errors programmatically:
General
INTERNAL_ERROR | An unexpected server error occurred |
VALIDATION_ERROR | One or more request parameters failed validation |
NOT_FOUND | Generic resource not found |
CONFLICT | Operation conflicts with current state |
Airports
AIRPORT_NOT_FOUND | No airport exists with the given identifier |
AIRPORT_DIAGRAM_NOT_FOUND | No airport diagram available for this airport |
RUNWAY_NOT_FOUND | No runway found matching the query |
COMMUNICATION_FREQUENCY_NOT_FOUND | No communication frequency found for this airport |
Weather
METAR_NOT_FOUND | No current METAR available for the given station |
TAF_NOT_FOUND | No current TAF available for the given station |
WEATHER_SERVICE_UNAVAILABLE | NOAA weather service is temporarily unavailable |
WEATHER_DATA_MISSING | Weather data was expected but not available |
NOTAMs & Airspace
NOTAM_SERVICE_UNAVAILABLE | FAA NOTAM service is temporarily unavailable |
AIRSPACE_NOT_FOUND | No airspace found matching the query |
OBSTACLE_NOT_FOUND | No obstacle found matching the query |
Documents
CHART_SUPPLEMENT_NOT_FOUND | No chart supplement available for this airport |
Performance & Navigation
PERFORMANCE_CALCULATION_ERROR | Error calculating performance values |
INVALID_PERFORMANCE_DATA | Provided performance data is invalid or out of range |
NAVLOG_CALCULATION_ERROR | Error computing the nav log |
External Services
EXTERNAL_SERVICE_UNAVAILABLE | A downstream service is temporarily unavailable |
Validation Errors
When a request fails validation (400 Bad Request), the response includes a validationErrors object with field-level details. Each key is the parameter name, and the value is an array of error messages:
{
"code": "VALIDATION_ERROR",
"message": "One or more validation errors occurred.",
"validationErrors": {
"latitude": [
"Latitude must be between -90 and 90 degrees"
],
"radiusNm": [
"Radius must be between 0 and 100 nautical miles"
]
},
"timestamp": "2026-01-15T18:56:00Z",
"traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"path": "/api/v1/notams/radius"
}Handling Errors
Because gateway and backend errors have different shapes, your error handling should check the response format. Here's how to handle both:
async function fetchMetar(icaoCode: string): Promise<Metar | null> {
const response = await fetch(
`https://api.preflightapi.com/api/v1/metars/${icaoCode}`,
{
headers: {
'Ocp-Apim-Subscription-Key': process.env.PREFLIGHT_API_KEY!,
},
},
)
if (!response.ok) {
const body = await response.json()
// Gateway errors have { statusCode, message }
// Tier-gating errors have { error }
// Backend errors have { code, message, traceId, ... }
switch (response.status) {
case 401:
// Gateway: invalid key
throw new Error(body.message)
case 403:
// Could be tier-gating ({ error }) or quota exceeded ({ statusCode, message })
throw new Error(body.error || body.message)
case 404:
// Backend: resource not found
if (body.code === 'METAR_NOT_FOUND') {
return null // No METAR available for this station
}
throw new Error(body.message)
case 429:
// Gateway: rate limited — check Retry-After header
throw new Error(body.message)
case 503:
// Backend: external service down — safe to retry
throw new Error(body.message)
default:
// Backend error — log traceId for support
console.error(`API error [${body.code}]: ${body.message}`)
if (body.traceId) console.error(`Trace ID: ${body.traceId}`)
throw new Error(body.message)
}
}
return response.json() as Promise<Metar>
}Retry Strategy
Not all errors are retryable. Here's a guide for which status codes to retry and which to handle immediately:
| Status | Source | Retryable? | Action |
|---|---|---|---|
400 | Backend | No | Fix the request — check parameters and validationErrors |
401 | APIM | No | Check your API key is correct and present |
403 (tier) | APIM | No | Upgrade your plan to access this endpoint |
403 (quota) | APIM | No | Wait for monthly quota reset or upgrade plan |
404 | Backend | No | The resource doesn't exist — check the identifier |
409 | Backend | No | Resolve the conflict |
429 | APIM | Yes | Wait for the Retry-After duration, then retry |
500 | Backend | Maybe | Retry once — if it persists, contact support with the traceId |
503 | Backend | Yes | External data source is down — retry with backoff |
For a ready-to-use retry implementation, see the exponential backoff example on the rate limits page.