Error Handling

PreflightAPI uses standard HTTP status codes and returns structured JSON error responses. Every error — whether generated by the APIM gateway (auth, rate limits, quotas, tier-gating) or the backend API (validation, not found, server errors) — uses the same response format.

Error Response Format

All errors return a unified ApiErrorResponse shape with a machine-readable code for programmatic handling and a traceId for support. Fields are omitted when null:

{
  "code": "METAR_NOT_FOUND",
  "message": "No current METAR available for station 'KXYZ'.",
  "timestamp": "2026-01-15T18:56:00.0000000Z",
  "traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "path": "/api/v1/metars/KXYZ"
}
FieldTypeDescriptionAlways Present
codestringMachine-readable error code (e.g., METAR_NOT_FOUND, RATE_LIMIT_EXCEEDED)Yes
messagestringHuman-readable description of what went wrongYes
timestampstringISO 8601 UTC timestamp of when the error occurredYes
traceIdstringCorrelation ID — include this when contacting supportYes
pathstringThe request path that generated the errorYes
servicestring?Name of the unavailable external service (only on 503 backend responses)No
retryAfterSecondsnumber?Seconds to wait before retrying (only on RATE_LIMIT_EXCEEDED)No
quotaResetsAtstring?ISO 8601 UTC timestamp when the monthly quota renews (only on QUOTA_EXCEEDED)No
validationErrorsobject?Field-level errors (only on VALIDATION_ERROR)No
detailsstring?Stack trace (development environments only)No

Gateway Errors (401, 403, 429, 502)

These errors are generated by the APIM gateway before the request reaches the backend. They use the same ApiErrorResponse format as backend errors.

Authentication (401)

{
  "code": "UNAUTHORIZED",
  "message": "Access denied due to invalid subscription key. Make sure to provide a valid key for an active subscription.",
  "timestamp": "2026-01-15T18:56:00.0000000Z",
  "traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "path": "/api/v1/metars/KJFK"
}

Tier-gating (403)

Returned when your subscription plan does not include access to the requested endpoint. See the endpoint access table for which endpoints are available on each plan.

{
  "code": "TIER_RESTRICTED",
  "message": "This endpoint is not available on the Student Pilot tier. Please upgrade to Private Pilot or higher.",
  "timestamp": "2026-01-15T18:56:00.0000000Z",
  "traceId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "path": "/api/v1/notams/KJFK"
}

Rate limit exceeded (429)

Returned when you exceed your per-minute rate limit. Includes a retryAfterSeconds field and a standard Retry-After header.

{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests. Please slow down and try again shortly.",
  "retryAfterSeconds": 45,
  "timestamp": "2026-01-15T18:56:00.0000000Z",
  "traceId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "path": "/api/v1/metars/KJFK"
}

Quota exceeded (429)

Returned when you exhaust your monthly API call quota. The quotaResetsAt field is an ISO 8601 UTC timestamp indicating when your quota renews.

{
  "code": "QUOTA_EXCEEDED",
  "message": "You have reached your monthly API call limit.",
  "quotaResetsAt": "2026-03-15T06:00:00.0000000Z",
  "timestamp": "2026-01-15T18:56:00.0000000Z",
  "traceId": "d4e5f6a7-b8c9-0123-defa-234567890123",
  "path": "/api/v1/metars/KJFK"
}

Note

Both rate limit and quota errors return 429. Use the code field to distinguish RATE_LIMIT_EXCEEDED (retryable after a short delay) from QUOTA_EXCEEDED (wait for monthly reset or upgrade).

Backend unavailable (502)

Returned when the gateway cannot reach the backend (e.g. during a deploy, crash, or network issue). Safe to retry with backoff.

{
  "code": "BACKEND_UNAVAILABLE",
  "message": "The API backend is temporarily unavailable. Please try again shortly.",
  "timestamp": "2026-01-15T18:56:00.0000000Z",
  "traceId": "e5f6a7b8-c9d0-1234-efab-345678901234",
  "path": "/api/v1/metars/KJFK"
}

Backend Errors (400, 404, 409, 499, 500, 503)

Errors generated by the API backend use domain-specific error codes for precise programmatic handling. A few examples:

// 404 — resource not found
{
  "code": "METAR_NOT_FOUND",
  "message": "No current METAR available for station 'KXYZ'.",
  "timestamp": "2026-01-15T18:56:00.0000000Z",
  "traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "path": "/api/v1/metars/KXYZ"
}
// 503 — external service unavailable (includes service field)
{
  "code": "EXTERNAL_SERVICE_UNAVAILABLE",
  "message": "The MagneticVariation service is temporarily unavailable.",
  "service": "MagneticVariationService",
  "timestamp": "2026-01-15T19:02:00.0000000Z",
  "traceId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "path": "/api/v1/navlog"
}

HTTP Status Codes

CodeNameSourceDescription
400Bad RequestBackendInvalid query parameters, missing required fields, or invalid values.
401UnauthorizedGatewayMissing or invalid API key. Check the Ocp-Apim-Subscription-Key header.
403ForbiddenGatewayEndpoint not available on your plan (tier-gating). Upgrade to access this endpoint.
404Not FoundBackendThe requested resource does not exist (e.g., unknown ICAO code, no METAR available).
409ConflictBackendThe request conflicts with the current state (e.g., duplicate resource).
429Too Many RequestsGatewayRate limit or monthly quota exceeded. Check the code field to distinguish RATE_LIMIT_EXCEEDED from QUOTA_EXCEEDED.
499Client Closed RequestBackendThe client disconnected before the server finished processing. No response body is sent.
500Internal Server ErrorBackendAn unexpected error occurred. Include the traceId when contacting support.
502Bad GatewayGatewayBackend is unreachable (deploy, crash, or network issue). Safe to retry with backoff.
503Service UnavailableBackendExternal data source down (service field identifies which).

Error Codes Reference

The code field contains one of these machine-readable values. Use these to handle specific errors programmatically:

Gateway

UNAUTHORIZEDMissing or invalid API subscription key
TIER_RESTRICTEDEndpoint not available on your subscription tier — upgrade to access
RATE_LIMIT_EXCEEDEDPer-minute rate limit exceeded — includes retryAfterSeconds field
QUOTA_EXCEEDEDMonthly call quota exhausted — includes quotaResetsAt field
BACKEND_UNAVAILABLEBackend is unreachable (deploy, crash, or network issue)

General

INTERNAL_ERRORAn unexpected server error occurred
VALIDATION_ERROROne or more request parameters failed validation
NOT_FOUNDGeneric resource not found
CONFLICTOperation conflicts with current state

Airports

AIRPORT_NOT_FOUNDNo airport exists with the given identifier
TERMINAL_PROCEDURE_NOT_FOUNDNo terminal procedures available for this airport
RUNWAY_NOT_FOUNDNo runway found matching the query
COMMUNICATION_FREQUENCY_NOT_FOUNDNo communication frequency found for this airport

Weather

METAR_NOT_FOUNDNo current METAR available for the given station
TAF_NOT_FOUNDNo current TAF available for the given station
WEATHER_SERVICE_UNAVAILABLENOAA weather service is temporarily unavailable
WEATHER_DATA_MISSINGWeather data was expected but not available

NOTAMs

NOTAM_NOT_FOUNDNo NOTAM found matching the query
NOTAM_SERVICE_UNAVAILABLEFAA NOTAM service is temporarily unavailable

Airspace & Obstacles

AIRSPACE_NOT_FOUNDNo airspace found matching the query
OBSTACLE_NOT_FOUNDNo obstacle found matching the query

NAVAIDs

NAVAID_NOT_FOUNDNo navigation aid found matching the query

Charts

CHART_SUPPLEMENT_NOT_FOUNDNo chart supplement available for this airport

Performance & Navigation

PERFORMANCE_CALCULATION_ERRORError calculating performance values
INVALID_PERFORMANCE_DATAProvided performance data is invalid or out of range
NAVLOG_CALCULATION_ERRORError computing the nav log

External Services

EXTERNAL_SERVICE_UNAVAILABLEA downstream service is temporarily unavailable — the service field identifies which one

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. This applies to both explicit parameter validation (e.g. coordinates, radius) and automatic model binding errors (e.g. malformed JSON, invalid enum values):

{
  "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:00.0000000Z",
  "traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "path": "/api/v1/notams/radius"
}

Handling Errors

Since all errors use the same response shape, you only need a single parsing path. Switch on the code field for specific handling:

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()
    // Every error has: { code, message, timestamp, traceId, path }

    switch (body.code) {
      case 'UNAUTHORIZED':
        throw new Error('Invalid API key')

      case 'TIER_RESTRICTED':
        throw new Error(body.message) // "...upgrade to Private Pilot..."

      case 'METAR_NOT_FOUND':
        return null // No METAR for this station — expected

      case 'RATE_LIMIT_EXCEEDED':
        // Retry after the indicated delay
        throw new Error(`Rate limited. Retry in ${body.retryAfterSeconds}s`)

      case 'QUOTA_EXCEEDED':
        // Monthly quota exhausted — not retryable
        throw new Error(`Quota exceeded. Resets at ${body.quotaResetsAt}`)

      case 'BACKEND_UNAVAILABLE':
        // Safe to retry with backoff
        throw new Error(body.message)

      case 'EXTERNAL_SERVICE_UNAVAILABLE':
        // body.service identifies which service is down
        throw new Error(`${body.message} (service: ${body.service})`)

      default:
        // Log traceId for support
        console.error(`API error [${body.code}]: ${body.message}`)
        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:

StatusCodeRetryable?Action
400VALIDATION_ERRORNoFix the request — check validationErrors
401UNAUTHORIZEDNoCheck your API key is correct and present
403TIER_RESTRICTEDNoUpgrade your plan to access this endpoint
404*_NOT_FOUNDNoThe resource doesn't exist — check the identifier
409CONFLICTNoResolve the conflict
429RATE_LIMIT_EXCEEDEDYesWait for retryAfterSeconds, then retry
429QUOTA_EXCEEDEDNoWait for quotaResetsAt or upgrade plan
500INTERNAL_ERRORMaybeRetry once — if it persists, contact support with traceId
502BACKEND_UNAVAILABLEYesBackend unreachable — retry with backoff
503*_UNAVAILABLEYesExternal service down — retry with backoff

For a ready-to-use retry implementation, see the exponential backoff example on the rate limits page.

Search Documentation

Search docs, endpoints, and schemas