# Error Handling

## Error Shape

All errors are returned in the standard GraphQL `errors` array with `extensions` containing a machine-readable `code`:

```json
{
  "errors": [
    {
      "message": "Authentication required",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["me"],
      "extensions": {
        "code": "UNAUTHENTICATED"
      }
    }
  ],
  "data": null
}
```

## HTTP Status Codes

GraphQL errors set the HTTP response status code via Yoga's `extensions.http.status`:

| HTTP Status | Extension Code | When |
|-------------|---------------|------|
| **401** | `UNAUTHENTICATED` | No valid session token in `Authorization` header |
| **401** | `NO_ACCOUNT` | Email not found in the system (service layer) |
| **401** | `INACTIVE` | User account is deactivated (service layer) |
| **403** | `FORBIDDEN` | User is authenticated but lacks the required permission |

## Authentication Errors (401)

| Extension Code | Message | When |
|---------------|---------|------|
| `UNAUTHENTICATED` | `Authentication required` | No valid Bearer token or expired session |
| `NO_ACCOUNT` | `No account found for this email.` | Service-layer auth check fails |
| `INACTIVE` | `Account is not active.` | User exists but is deactivated |

These are thrown by `requireAuth()` guards and `AuthError` in service modules.

## Permission Errors (403)

| Extension Code | Message | When |
|---------------|---------|------|
| `FORBIDDEN` | `Missing required permission: <key>` | User lacks the required permission for the operation |

Example: `Missing required permission: users.read`

These are thrown by `requirePerm()` guards and `PermissionError` in service modules.

## Auth Flow Errors (REST)

The OAuth callback endpoints (`/auth/google/callback`, `/auth/microsoft/callback`) return JSON errors:

| HTTP Status | Error Code | Message | When |
|-------------|-----------|---------|------|
| 400 | — | `Invalid OAuth state` | CSRF state mismatch or expired OAuth flow |
| 403 | `NO_ACCOUNT` | `No account found for this email. You must be invited.` | Email not in the system |
| 403 | `INACTIVE` | `Your account is deactivated. Contact an administrator.` | User exists but is deactivated with no pending invitation |

## Business Logic Errors

| Message | When |
|---------|------|
| `User with this email already exists and is active.` | Trying to invite an already active user |
| `User not found` | Operating on a non-existent user ID |
| `Role not found` | Operating on a non-existent role ID |
| `Cannot modify a system role` | Trying to update the Superadmin role |
| `Cannot delete a system role` | Trying to delete the Superadmin role |

## Frontend Handling

For frontend agents implementing error handling:

1. Check `response.errors` array in every GraphQL response
2. Use `extensions.code` for machine-readable categorization:
   - `UNAUTHENTICATED` (HTTP 401) → redirect to login
   - `FORBIDDEN` (HTTP 403) → show "access denied" UI
   - `INTERNAL_SERVER_ERROR` → generic error, retry or show toast
3. Check the HTTP status code — 401 and 403 are set automatically
4. For OAuth callbacks, check the JSON `error` field:
   - `NO_ACCOUNT` → show "you need an invitation" message
   - `INACTIVE` → show "account deactivated" message
