Skip to main content

Resolver errors arrive with HTTP 200

This is the single most important thing to know before you write error handling. Following the GraphQL over HTTP specification, a request that reaches our resolvers returns HTTP 200 even when an operation failed. The failure is reported inside the response body, not by the status code. So this check does not work:
Check the body instead. A few transport errors, such as 415 for an unsupported Content-Type, have an empty body, so check that the response is JSON before parsing it:
Some failures are caught before anything runs. None of them reaches a resolver or uses an API call:
  • A malformed request returns an error status: HTTP 400 for invalid JSON or a missing query, 413 for a body over 64 KB, 405 for a method other than POST, and 415 for an unsupported Content-Type.
  • A query that is not valid for the schema returns HTTP 400 only if your request sends Accept: application/graphql-response+json. This covers a syntax error, an unknown field, a wrong argument type, an invalid enum value, and a query over the query limits. If you send Accept: application/json, or no Accept header, the same error returns HTTP 200. Either way, the body has an errors array and no data.
Once execution begins, resolver-level input validation and API failures — including authentication, authorization, upstream rate limits, and upstream 5xx responses — return HTTP 200 with an errors array.

The error shape

Every resolver error carries its HTTP-like API status in extensions:
  • message — a human-readable description. We remove API keys and other credentials from resolver error messages, but an error about a value you sent, such as a variable of the wrong type, can repeat that value. Treat message as possibly sensitive before you log it.
  • path — which field in your query failed. With several fields in one request, this is how you tell which.
  • extensions.status — the HTTP status the equivalent REST call would have returned. Switch on this.
  • extensions.code — the numeric Ayrshare error code, when the underlying endpoint supplies one.
  • extensions.retryAfter — present on upstream quota or rate-limit 429s when a retry delay is available. It is not present on the per-request dispatch-budget 429.
Use extensions.code with the error code reference, or pass it to explainError.

Partial success

If a request asks for several things and only some succeed, you get both data and errors:
history succeeded and its data is usable. Only analytics needs retrying. Do not discard the whole response because errors is present — that would throw away data you have already been billed for.

Partial success on multi-network posts

A post to several networks can succeed on some and fail on others. This is not reported as a GraphQL error, because the operation itself worked — it did exactly what you asked and is telling you what happened. The result separates the three outcomes:
  • postIds — networks that published, with their post URLs. Successes only.
  • errors — networks that failed, with the reason.
  • blocked — networks stopped before an attempt, for example by the per-network circuit breaker after repeated deterministic failures. These are separate from provider attempts that returned errors.
status is "error" if any network failed, so it is not a reliable signal that nothing published. Read postIds to find out what did.

Retries

The GraphQL API never retries automatically. If a request fails, nothing was silently attempted again on your behalf. This matters most for mutations. createPost exposes idempotencyKey: generate a unique key before the first attempt, then reuse the same key and identical payload for every retry. A settled retry replays the original result instead of repeating the write. Do not switch to a new key after an ambiguous response. boostFacebookPost and instagramBoostPost also take an idempotencyKey, and it protects real money. A retry with the same key and the same arguments returns the original result, marked idempotentReplayed: true, instead of creating and paying for a second ad. One key covers your whole account on both Meta platforms, so never reuse a Facebook boost’s key for an Instagram boost. When a key is refused, extensions.status tells you why. createPost returns 409 while the first attempt is still processing, so wait and retry with the same key, and 400 when the payload differs from the first attempt. The boost mutations return 409 in both cases, and also when the key was used for a boost on the other platform. Other writes, such as addComment, have no idempotency key. If their response is lost, check the social network or API state before deciding whether another attempt is safe. Two different limits both report 429, and they need different handling. The dispatch budget only rejects root fields once five API calls have already started in the same request, and its message begins Query exceeds the per-request dispatch budget: split those fields into requests of at most five, with no need to wait. Any other 429 is a rate limit or quota: wait extensions.retryAfter seconds when it is present, otherwise back off before retrying, and retry only the fields that failed. For a 5xx on a read, retrying is safe, with one exception: generatePost is a query, but a retry calls the AI generator again, counts as another API call, and returns different text. For a 5xx on a write without idempotency, reconcile whether it took effect before retrying.

Common statuses

Errors raised by GraphQL itself before execution, such as an unknown field, a wrong argument type, or an invalid enum value, have no extensions.status. They return HTTP 400 or 200 depending on your Accept header, as described above. They mean the query itself is wrong, and no API call was made or billed.