---
slug: vectors/errors
title: Errors
kind: reference
surface: vectors
summary: The Aetherfy vector API error envelope, every stable error code with its HTTP status and retryability, and the exception classes the Python and JavaScript SDKs raise for each.
sources:
  - vectordb:backend/middleware/errorHandler.js
  - vectordb:backend/middleware/auth.js
  - vectordb:backend/routes/proxy.js
  - aetherfy-vectors-python-sdk:aetherfy_vectors/exceptions.py
  - aetherfy-vectors-js-sdk:src/exceptions.ts
  - aetherfy-vectors-js-sdk:src/auth.ts
---

# Errors in the Aetherfy vector database

## The Aetherfy error envelope

Every error the Aetherfy vector API authors is returned in one shape:

```json
{
  "error": {
    "code": "STABLE_CODE",
    "message": "human readable description"
  }
}
```

`code` is the stable, machine-readable identifier — branch on it, never on the
message text, which is written for humans and may be reworded. Alongside the two
required fields, an Aetherfy error may carry extras that depend on the site that
raised it:

| Extra field | Appears on |
|---|---|
| `details` | Errors with structured context |
| `field`, `max` | Validation failures against a numeric cap |
| `request_id` | Correlation identifier for support |
| `documentation_url` | Errors with a canonical explanation |
| `collection_name` | Collection-scoped failures |
| `existing_regions` | A collection that lives in another region |
| `agents` | A collection still referenced by agents |
| `size_mb`, `max_size_mb` | Response-size failures |
| `current_etag` | Schema compare-and-set conflicts |
| `retriable` | Errors Aetherfy marks as safe to retry |

## The Aetherfy error envelope is universal

Every path authors the envelope, including the storage-engine pass-through
routes. Those routes proxy Qdrant, whose native failure body is shaped
`{"status": {"error": "..."}, "time": 0.001}` — Aetherfy normalises it on the way
out rather than relaying it, so `error.code` is always present and a generic
handler can rely on it.

Earlier versions of these docs described `POST /api/v1/collections/{name}/points/retrieve`
as an exception that relayed the engine body verbatim. That is no longer true.
If you wrote a handler tolerating a missing `error.code` on that route, the
tolerance is now dead code — harmless, but you can drop it.

## Error codes in the Aetherfy vector API

Every code Aetherfy can return, with its HTTP status and whether retrying the
identical request can succeed.

| Code | HTTP | Meaning | Retryable |
|---|---|---|---|
| `MISSING_API_KEY` | 401 | No `Authorization` header, or not a `Bearer` header | no |
| `INVALID_API_KEY` | 401 | The key does not resolve | no |
| `ACCOUNT_SUSPENDED` | 403 | The account is suspended for billing | no |
| `COLLECTION_OWNERSHIP_MISMATCH` | 403 | The collection belongs to another account | no |
| `NOT_FOUND` | 404 | Unsupported endpoint, or missing collection | no |
| `COLLECTION_NOT_FOUND` | 404 | No such collection | no |
| `SCHEMA_NOT_DEFINED` | 404 | The collection has no stored schema | no |
| `METHOD_NOT_ALLOWED` | 405 | The method is not supported on that path | no |
| `VALIDATION_ERROR` | 400 | A request field failed validation | no |
| `INVALID_POINT_ID` | 400 | A point id is not an unsigned integer or UUID | no |
| `TOO_MANY_POINTS` | 400 | More than 10 000 points in one upsert | no |
| `RESERVED_FIELD` | 400 | A payload key begins `__aetherfy_` | no |
| `MALFORMED_JSON` | 400 | The body is not valid JSON | no |
| `SCHEMA_VALIDATION_FAILED` | 400 | The payload violates a strictly-enforced schema | no |
| `COLLECTION_LIMIT_EXCEEDED` | 400 | The tier's collection cap is reached | no |
| `INVALID_ENFORCEMENT_MODE` | 400 | Not `off`, `warn`, or `strict` | no |
| `INVALID_SAMPLE_SIZE` | 400 | Outside 100 to 10 000 | no |
| `REQUEST_IDLE_TIMEOUT` | 408 | 30 s elapsed with no bytes on an upsert | no |
| `COLLECTION_EXISTS_IN_OTHER_REGION` | 409 | The collection already lives elsewhere | no |
| `COLLECTION_NAME_TAKEN` | 409 | The name is in use by a collection **with a different configuration** — a re-create with the same configuration is idempotent and returns 200 | no |
| `COLLECTION_IN_USE` | 409 | An agent still references the collection | no |
| `CLEANUP_IN_PROGRESS` | 409 | A prior deletion is still finishing — carries `retriable: true` | **yes** |
| `SCHEMA_VERSION_MISMATCH` | 412 | `If-Match` ETag conflict; body carries `current_etag` | no |
| `PAYLOAD_TOO_LARGE` | 400 / 413 | Two causes, two statuses: **400** when a single point's payload exceeds 64 KB, **413** when the upsert wire body exceeds the 500 MB ceiling | no |
| `RESPONSE_TOO_LARGE` | 413 | The response would exceed 10 MB | no |
| `COLLECTION_REGIONS_EMPTY` | 422 | No regions resolved for the request | no |
| `COLLECTION_REGIONS_NOT_IN_SCOPE` | 422 | Requested regions are outside the plan's scope | no |
| `STARTER_REGION_CONSISTENCY` | 422 | Single-region plan; the resource asked for a different region | no |
| `RATE_LIMIT_EXCEEDED` | 429 | The tier's per-minute request cap is exceeded | **yes** |
| `STORAGE_LIMIT_EXCEEDED` | 429 | The tier's storage cap is exceeded — free space first | no |
| `INTERNAL_ERROR` | 500 | Unhandled server-side failure | no |
| `PROXY_ERROR` | 502 | The proxy could not complete the upstream call | **yes** |
| `SERVICE_UNAVAILABLE` | 503 | The service is temporarily unable to serve | **yes** |
| `GATEWAY_TIMEOUT` | 504 | The upstream call timed out | **yes** |

## Aetherfy's fallback error codes

The codes above are authored deliberately: something in Aetherfy decided that
exact failure and named it. The codes below are different in kind. Aetherfy
proxies a storage engine and authors the error envelope on the way out — when
the upstream body carries a usable code, that code is preserved; when it does
not, Aetherfy synthesises one from the HTTP status.

Seeing one of these means **the upstream failed without naming its own reason**,
so the `message` carries more signal than the code. Treat them as "something
upstream went wrong, here is roughly what kind" rather than as specific
diagnoses.

| Code | HTTP | Meaning | Retryable |
|---|---|---|---|
| `BAD_REQUEST` | 400 | The engine rejected the request and supplied no code. Read the message | no |
| `UNAUTHORIZED` | 401 | An upstream component refused credentials without naming a code | no |
| `FORBIDDEN` | 403 | An upstream component denied the request without naming a code | no |
| `CONFLICT` | 409 | A conflicting state upstream, unnamed. The specific 409s above are more precise | no |
| `PRECONDITION_FAILED` | 412 | A precondition failed upstream, unnamed — typically an ETag mismatch | no |
| `RATE_LIMITED` | 429 | A 429 from upstream carrying no code of its own | **yes** |
| `UPSTREAM_ERROR` | 500 / 502 / 503 / 504 | The catch-all: an unmapped upstream status with no code | **yes** |

### RATE_LIMITED is not RATE_LIMIT_EXCEEDED

Both exist, both are 429, and they mean different things. Branch on the code, not
the status:

| Code | Who produced it | What it tells you |
|---|---|---|
| `RATE_LIMIT_EXCEEDED` | Aetherfy's own per-minute rate limiter | You exceeded your plan's request allowance. Back off; the allowance resets on the next minute bucket |
| `RATE_LIMITED` | The envelope normaliser, from an upstream 429 | Something upstream of Aetherfy throttled the request without naming a code. Not necessarily your plan allowance |

A retry loop keyed on `status === 429` will treat these identically, which is
usually fine — both want backoff. A dashboard or alert that reports "you hit your
rate limit" should key on `RATE_LIMIT_EXCEEDED` specifically, or it will
misattribute an upstream throttle to the customer's plan.

## Which Aetherfy errors are worth retrying

Only five Aetherfy codes can succeed on an identical retry. Everything else is a
statement about the request or the account, and repeating it verbatim produces
the same answer.

| Retryable code | HTTP | How to retry against Aetherfy |
|---|---|---|
| `CLEANUP_IN_PROGRESS` | 409 | Wait for the prior deletion to finish; the body carries `retriable: true` |
| `RATE_LIMIT_EXCEEDED` | 429 | Back off on your own schedule — Aetherfy sends no `Retry-After` header |
| `PROXY_ERROR` | 502 | Retry with backoff |
| `SERVICE_UNAVAILABLE` | 503 | Retry with backoff |
| `GATEWAY_TIMEOUT` | 504 | Retry with backoff |

`STORAGE_LIMIT_EXCEEDED` is a 429 but is **not** retryable: nothing frees itself
while you wait. Delete data or raise the cap. See [limits](/vectors/limits).

## Exception classes in the Aetherfy Python SDK

The Aetherfy Python SDK maps API errors onto typed exceptions. The base class is
`AetherfyVectorsException`; catch it to catch everything the SDK authors.

| Attribute on the base class | Meaning |
|---|---|
| `.message` | Human-readable description |
| `.request_id` | Correlation identifier |
| `.status_code` | HTTP status |
| `.details` | Structured context, when present |
| `.error_code` | The stable code from the envelope |

| Exception | Extra attributes |
|---|---|
| `AetherfyVectorsException` | base class |
| `AuthenticationError` | — |
| `RateLimitExceededError` | `.retry_after` |
| `ServiceUnavailableError` | — |
| `ValidationError` | — |
| `CollectionNotFoundError` | — |
| `PointNotFoundError` | — |
| `RequestTimeoutError` | — |
| `NetworkError` | — |
| `SchemaValidationError` | `.errors` |
| `SchemaNotFoundError` | — |
| `CollectionInUseError` | — |
| `CollectionInOtherRegionError` | `.existing_regions`, `.requesting_region` |
| `QuotaExceededError` | `.quota_type`, `.current`, `.limit` |
| `PartialUpsertError` | `.saved`, `.total`, `.failed` |

`.retry_after` on `RateLimitExceededError` is an attribute of the class, not a
value Aetherfy supplies: Aetherfy sends no `Retry-After` header on a 429, so
there is nothing to populate it from. Do not branch on it — back off on your own
schedule.

The helper `is_retryable_error(error)` lives in `aetherfy_vectors.exceptions` and
answers the retry question for you.

```python
import os
import time

from aetherfy_vectors import (
    AetherfyVectorsClient,
    AetherfyVectorsException,
    AuthenticationError,
    QuotaExceededError,
    RateLimitExceededError,
)
from aetherfy_vectors.exceptions import is_retryable_error

client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])

for attempt in range(3):
    try:
        results = client.search("articles", query_vector=[0.1, 0.2, 0.3, 0.4], limit=5)
        print([hit.id for hit in results])
        break
    except AuthenticationError as err:
        print("bad key, not retrying:", err.message)
        break
    except QuotaExceededError as err:
        print("quota:", err.quota_type, err.current, err.limit)
        break
    except RateLimitExceededError as err:
        print("rate limited, request_id:", err.request_id)
        time.sleep(2 ** attempt)
    except AetherfyVectorsException as err:
        if not is_retryable_error(err):
            print("permanent failure:", err.error_code, err.status_code)
            break
        time.sleep(2 ** attempt)

client.close()
```

## Exception classes in the Aetherfy JavaScript SDK

The Aetherfy JavaScript base class is `AetherfyVectorsError` — note the different
name from Python's `AetherfyVectorsException`.

| Member on the base class | Meaning |
|---|---|
| `.code` | The stable code from the envelope |
| `.requestId` | Correlation identifier |
| `.statusCode` | HTTP status |
| `.details` | Structured context, when present |
| `toJSON()` | Serialisable form of the error |

| Exception | Extra members |
|---|---|
| `AetherfyVectorsError` | base class |
| `AuthenticationError` | — |
| `RateLimitExceededError` | `.retryAfter` |
| `ServiceUnavailableError` | — |
| `ValidationError` | — |
| `CollectionNotFoundError` | — |
| `PointNotFoundError` | — |
| `RequestTimeoutError` | — |
| `NetworkError` | — |
| `ConflictError` | no Python twin |
| `CollectionInOtherRegionError` | — |
| `QuotaExceededError` | — |
| `CollectionInUseError` | — |
| `SchemaNotFoundError` | — |
| `SchemaValidationError` | — |
| `PartialUpsertError` | — |

`.retryAfter` on the Aetherfy JavaScript `RateLimitExceededError` is the same
story as Python's `.retry_after`: the member exists on the class, but because
Aetherfy sends no `Retry-After` header there is nothing to populate it from. Do
not branch on it — back off on your own schedule.

Three utilities ship alongside them from the Aetherfy package root:
`createErrorFromResponse`, `isAetherfyVectorsError`, and `isRetryableError`.

```typescript
import {
  AetherfyVectorsClient,
  AetherfyVectorsError,
  AuthenticationError,
  QuotaExceededError,
  RateLimitExceededError,
  isAetherfyVectorsError,
  isRetryableError,
} from 'aetherfy-vectors';

const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

for (let attempt = 0; attempt < 3; attempt += 1) {
  try {
    const results = await client.search('articles', [0.1, 0.2, 0.3, 0.4], { limit: 5 });
    console.log(results.map((hit) => hit.id));
    break;
  } catch (err) {
    if (err instanceof AuthenticationError) {
      console.log('bad key, not retrying');
      break;
    }
    if (err instanceof QuotaExceededError) {
      console.log('quota exceeded');
      break;
    }
    if (err instanceof RateLimitExceededError) {
      console.log('rate limited, requestId:', err.requestId);
      await sleep(2 ** attempt * 1000);
      continue;
    }
    if (isAetherfyVectorsError(err)) {
      if (!isRetryableError(err)) {
        const e = err as AetherfyVectorsError;
        console.log('permanent failure:', e.code, e.statusCode);
        break;
      }
      await sleep(2 ** attempt * 1000);
      continue;
    }
    throw err;
  }
}

client.dispose();
```

## The duplicate AuthenticationError in the Aetherfy JavaScript SDK

The Aetherfy JavaScript package defines **two** classes named
`AuthenticationError`, in two modules. To keep the package root unambiguous, the
one declared in `src/auth.ts` is re-exported under a different name:

| Declared in | Root export name |
|---|---|
| `src/exceptions.ts` | `AuthenticationError` |
| `src/auth.ts` | `AuthError` |

So `AuthenticationError` imported from the Aetherfy package root is the
transport-level error raised when the API rejects a key, and `AuthError` is the
key-management one. An `instanceof` check against the wrong class silently never
matches, so import the name that corresponds to the layer you are catching.

## Mapping Aetherfy error names across the two SDKs

| Concept | Python | JavaScript |
|---|---|---|
| Base class | `AetherfyVectorsException` | `AetherfyVectorsError` |
| Stable code | `.error_code` | `.code` |
| Correlation id | `.request_id` | `.requestId` |
| HTTP status | `.status_code` | `.statusCode` |
| Rate-limit attribute (never populated — Aetherfy sends no `Retry-After` header) | `.retry_after` | `.retryAfter` |
| Retryability helper | `is_retryable_error` (in `aetherfy_vectors.exceptions`) | `isRetryableError` (package root) |
| Type guard | — | `isAetherfyVectorsError` |
| Conflict class | — | `ConflictError` |

The caps and tier limits behind most 4xx codes are on [limits](/vectors/limits);
the routes that raise them are on [the REST API reference](/vectors/api).
