# Errors

Every API error has the same shape. Branch on its code, never on its message.

## The shape

A success is `{"data": …}`. A failure is `{"error": …}`, even for a mistyped path (`404 NOT_FOUND`) or a wrong method (`405 METHOD_NOT_ALLOWED`).

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "…written for a person; may change",
    "details": { "field": "name" }
  }
}
```

Every answer carries an `X-Request-ID` header. Quote it when you report a problem. On a self-hosted installation, it is the `request_id` in the [logs](https://docs.gorunner.io/self-host/logs).

- `code`: A stable name in capitals, such as `CONCURRENCY_LIMIT`. Branch on it.
- `message`: One sentence for a person. It uses the installation’s own words and may change.
- `details`: An object, on some errors: what a program can act on, such as the wrong `field`, the missing `permission`, the `switch` that is off or why a token ended (`reason`). Its keys depend on the code.

## Branch on the code

Decide by status first, then by code. Retry only what says to retry, and wait as long as `Retry-After` asks when it is sent.

**JavaScript**

```js
const res = await fetch(url, { method: "POST", headers, body });
const answer = await res.json();

if (!res.ok) {
  const { code, message } = answer.error;
  const retryAfter = res.headers.get("Retry-After");
  if (retryAfter) {
    // a 429 or 503 that says when: try again after that many seconds
  } else if (res.status === 429) {
    // CONCURRENCY_LIMIT or RUNS_WAITING_LIMIT: try again when a run finishes
  } else if (code === "PAUSED") {
    // a self-hosted installation without a license: don't retry
  } else if (code === "CONFLICT") {
    // publish the workflow first, or wait for the other request
  } else {
    throw new Error(`${code}: ${message}`); // fix the request
  }
}
```

**Python**

```python
res = requests.post(url, headers=headers, json=body)

if not res.ok:
    error = res.json()["error"]
    retry_after = res.headers.get("Retry-After")
    if retry_after:
        pass  # a 429 or 503 that says when: try again after that many seconds
    elif res.status_code == 429:
        pass  # CONCURRENCY_LIMIT or RUNS_WAITING_LIMIT: try when a run finishes
    elif error["code"] == "PAUSED":
        raise RuntimeError("Load a license first")  # don't retry
    elif error["code"] == "CONFLICT":
        pass  # publish the workflow first, or wait
    else:
        raise RuntimeError(f"{error['code']}: {error['message']}")
```

## What to do, by status

| Status | What happened | What to do |
| --- | --- | --- |
| 400, 422 | The request or the workflow is not valid. For a workflow, [Check a definition](https://docs.gorunner.io/api/workflows/validate-workflow-definition) lists every problem and what is allowed. | Fix the request |
| 401 | The token is missing, expired or revoked. | Make a new token |
| 403 | The token may not do this, or the account is suspended. | Check its permissions |
| 404 | Nothing with that ID in the token’s workspace. | Check the ID |
| 405 | The path exists, but not with this method. `Allow` lists the ones it takes. | Use another method |
| 409 | The thing is in the wrong state, such as not published. Or someone saved the workflow since you read it (`expectedRev`). | Change its state, or read it again |
| 413 | The body is over 5 MiB (5,242,880 bytes). Malformed JSON is `400`, not `413`. | Send less |
| 415 | `INVALID_CONTENT_TYPE`: a `Content-Type` other than `application/json`. | Send JSON |
| 429 | A limit was reached. | Retry after `Retry-After`. `CONCURRENCY_LIMIT` and `RUNS_WAITING_LIMIT` send none: retry when a run finishes. |
| 500 | `INTERNAL_ERROR`: something failed on GoRunner’s side. | Retry. If it repeats, send us its `X-Request-ID` |
| 503 | GoRunner couldn’t do it just now, the server is holding as much request data as it can (`BUSY`), or the installation is paused. Nothing was done. | Retry after `Retry-After`. `PAUSED` sends none: load a license. |

A `503 SERVICE_UNAVAILABLE` means the database or Redis didn’t answer, so your token and the address weren’t checked. Only a token or an address that really isn’t there answers `401` or `404`.

[Error codes](https://docs.gorunner.io/errors?kind=api) lists all 75 codes, with the calls that send each one.

## When an installation is paused

A self-hosted installation runs only while a valid, unexpired license is loaded. Without one, a call with an API token made in GoRunner answers `503 PAUSED`, and nothing is read, written or run. A token made inside a partner’s product answers `403 EMBED_NOT_AVAILABLE` instead.

Calls work again within a second of loading a license, or within a minute if Redis is down. GoRunner Cloud never pauses.

Related: [Idempotency](https://docs.gorunner.io/api/idempotency) · [Authentication](https://docs.gorunner.io/api/authentication) · [Error codes](https://docs.gorunner.io/errors) · [Check a definition](https://docs.gorunner.io/api/workflows/validate-workflow-definition) · [Workflow definition](https://docs.gorunner.io/api/workflow-definition) · [Your license](https://docs.gorunner.io/self-host/license)

Source: https://docs.gorunner.io/api/errors
