Skip to main content

guides/error-handling.md

# Error handling

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

## Reasons

`reason` is one of:

- a `GhEx.Error` for an API error response, or
- a `Req` exception (for example `Req.TransportError`) for a transport failure.

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

```elixir
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:

```elixir
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`:

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

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