Every GhEx.REST and GhEx.GraphQL call returns {:ok, body, meta} on success or {:error, reason} on failure.

Reasons

reason is one of:

GhEx.Error carries :status, :message, :body, :errors, and :documentation_url:

case GhEx.REST.get(client, "/repos/o/does-not-exist") do
  {:ok, repo, _meta} -> repo
  {:error, %GhEx.Error{status: 404}} -> :not_found
  {:error, %GhEx.Error{} = err} -> {:error, Exception.message(err)}
  {:error, exception} -> {:transport_error, exception}
end

GraphQL errors

GraphQL answers with HTTP 200 even on failure, so a response carrying a non-empty errors array becomes {:error, %GhEx.Error{}} (the same struct REST uses). Any partial data is preserved on the error's :body as %{"data" => ..., "errors" => ...}.

Errors as exceptions

GhEx.Error is also an exception, so the streaming helpers that cannot return an :error tuple raise it:

try do
  client |> GhEx.REST.stream("/repos/o/r/issues") |> Enum.to_list()
rescue
  e in GhEx.Error -> {:error, e.status}
end

Rate limits

Req already retries ordinary transient errors. To also back off on GitHub's secondary rate limits (a 403 with retry-after or x-ratelimit-remaining: 0), opt into GhEx.RateLimit.retry/2:

GhEx.new(
  auth: {:token, token},
  req_options: [retry: &GhEx.RateLimit.retry/2]
)

Req bounds the attempts with :max_retries (default 3).