Error Handling¶
Every exception the SDK defines derives from PineconeError, so except PineconeError
is the one blanket catch, and it is the right catch for anything that reaches the
network.
It is not a catch for calling the SDK wrongly. A misspelled or unsupported keyword
argument raises a plain built-in TypeError naming the argument, not a
PineconeTypeError:
TypeError: Index() got unexpected keyword arguments: ['retry_config']
AsyncPinecone(proxy_headers=...) likewise raises NotImplementedError — the sync
Pinecone accepts that argument and the async client does not. Neither is a
PineconeError. Both are programming mistakes that fail on the first call, so they
belong in your tests rather than in an except block.
Every class below is importable from the package root or from pinecone.errors;
the two paths reach the same class.
from pinecone import NotFoundError
from pinecone.errors import NotFoundError # same class
Exception Hierarchy¶
PineconeError
├── ApiError # the server answered with an HTTP error status
│ ├── UnauthorizedError # 401
│ ├── PaymentRequiredError # 402
│ ├── ForbiddenError # 403
│ ├── NotFoundError # 404
│ ├── ConflictError # 409
│ ├── FailedPreconditionError # 412
│ ├── RateLimitError # 429
│ └── ServiceError # 5xx
├── PineconeValueError # also a built-in ValueError
├── PineconeTypeError # also a built-in TypeError
├── PineconeConnectionError # the request never got an answer
├── PineconeTimeoutError # also a built-in TimeoutError
├── ResponseParsingError # the answer arrived and could not be decoded
├── IndexInitFailedError # a polled index entered InitializationFailed
└── IndexTerminatedError # a polled index entered Terminating or Disabled
Three of these multiply inherit from a Python built-in, which changes what your existing handlers catch:
Class |
Also inherits |
Consequence |
|---|---|---|
|
|
An existing |
|
|
An existing |
|
|
An existing |
The inheritance runs one way only. except PineconeError does not catch a plain
ValueError or TimeoutError raised by something else in your stack.
A status the SDK does not have a subclass for — a 400 from a request the server
rejected, for instance — raises ApiError itself. Catch ApiError if you want
every server-side failure; catch a subclass to single one out.
Full API reference, including each class’s attributes: Exceptions.
Who Raises What¶
The SDK validates arguments before it opens a socket. Anything it can decide locally, it decides locally, so a bad namespace name or an empty ID list costs no round trip:
Raised by |
Class |
Cause |
|---|---|---|
The SDK, before the request |
|
An argument’s value is not one the API accepts: an empty name, a namespace outside the allowed charset, a count outside the allowed range, or two mutually exclusive selectors passed together |
The SDK, before the request |
|
An argument’s type is wrong: a non-string vector ID, non-dict metadata, or a request body that cannot be JSON-encoded |
The transport |
|
DNS failure, connection refused, TLS failure, a reset mid-stream — the request never got an answer |
The transport |
|
A connect, read, write, or pool deadline elapsed |
The server |
|
The request reached Pinecone and came back with an error status |
The SDK, after the response |
|
The response arrived but did not decode into the expected shape |
PineconeConnectionError and PineconeTimeoutError can come out of any method
that talks to Pinecone. No method’s own documentation repeats them, because the
answer is the same everywhere: the call did not complete, and whether the write
landed is unknown.
Catching Specific Errors¶
from pinecone import Pinecone
from pinecone.errors import (
NotFoundError,
ConflictError,
UnauthorizedError,
ForbiddenError,
PaymentRequiredError,
FailedPreconditionError,
RateLimitError,
ServiceError,
PineconeConnectionError,
PineconeTimeoutError,
)
pc = Pinecone()
try:
pc.indexes.describe("nonexistent-index")
except NotFoundError:
print("Index does not exist")
except ConflictError:
print("Operation conflicts with current state")
except UnauthorizedError:
print("Invalid or missing API key")
except ForbiddenError:
print("API key lacks permission for this operation")
except PaymentRequiredError as exc:
print(f"Billing blocks this operation: {exc.message}")
except FailedPreconditionError as exc:
print(f"Resource is not in a state that permits this: {exc.message}")
except RateLimitError:
print("Rate limited; backing off before retrying")
except ServiceError as exc:
print(f"Server error {exc.status_code}: {exc.message}")
except PineconeConnectionError:
print("Network error; check your connection")
except PineconeTimeoutError:
print("Request timed out")
Order matters: except ApiError before any of these would swallow all of them.
ApiError Attributes¶
ApiError and every subclass carry structured context:
Attribute |
Type |
Description |
|---|---|---|
|
|
The HTTP status the server returned |
|
|
The server’s own error message, already unwrapped from the response body |
|
|
The parsed JSON response body, or |
|
|
HTTP reason phrase |
|
|
Response headers |
|
|
The server’s machine-readable error code, when the body carries one |
|
|
Request ID, for correlating with Pinecone support |
str(exc) renders the ones that matter: [404 NOT_FOUND] message (request_id: ...).
from pinecone.errors import ApiError
try:
pc.indexes.describe("my-index")
except ApiError as exc:
print(exc.status_code)
print(exc.message)
print(exc.request_id)
Reading exc.body["error"]["message"] yourself is redundant — that is where
exc.message comes from. Reach for body only for fields the SDK does not lift
out.
RateLimitError adds one attribute of its own: retry_after, the response’s
Retry-After header as a number of seconds. It is None when the header was
absent, negative, or an HTTP-date, which the SDK does not parse.
Retry It, or Give Up¶
Whether an exception is worth a second attempt is a property of the class, not of the call site:
Class |
Retry by hand? |
|---|---|
|
Yes, once the SDK’s own retries are spent — but note the write may have landed |
|
Yes, after |
|
Yes |
|
Yes, once you satisfy the precondition — |
|
Only after resolving the conflict; a bare retry answers 409 again |
|
No, with one exception — see When 404 does not mean “not found” |
|
No. Fix the key or its permissions |
|
No. An organization owner has to resolve the billing state |
|
No. The request was never sent; fix the argument |
|
No |
The SDK already retries the transient half of that table for you, several times, before you ever see the exception. Reaching one of them means the built-in retries were exhausted, so an immediate re-call is unlikely to help — see Retries and Resilience for what it already tried and how long it waited.
Retrying a delete deserves its own note. Control-plane deletes are not
idempotent in the “second call also succeeds” sense: pc.indexes.delete() and
admin.role_bindings.delete() both answer 404 once the resource is gone. So if you
wrap a delete in your own retry loop, treat NotFoundError as success rather than
as failure.
ConflictError when creating an index¶
If you call pc.indexes.create() and an index with that name already exists, the
server answers 409 and the SDK raises ConflictError. The idiomatic fix is to
guard the create call with pc.indexes.exists():
from pinecone import Pinecone
pc = Pinecone()
if not pc.indexes.exists("my-index"):
pc.indexes.create(
name="my-index",
schema={"fields": {"embedding": {"type": "dense_vector", "dimension": 1536, "metric": "cosine"}}},
deployment={"deployment_type": "managed", "cloud": "aws", "region": "us-east-1"},
)
exists() returns True if an index with that name is present, False otherwise.
If you genuinely cannot check first — concurrent callers race between the check and
the create — catch ConflictError and treat it as a no-op:
from pinecone import Pinecone
from pinecone.errors import ConflictError
pc = Pinecone()
try:
pc.indexes.create(
name="my-index",
schema={"fields": {"embedding": {"type": "dense_vector", "dimension": 1536, "metric": "cosine"}}},
deployment={"deployment_type": "managed", "cloud": "aws", "region": "us-east-1"},
)
except ConflictError:
pass # index already exists, nothing to do
When 404 does not mean “not found”¶
pc.restore_jobs.describe() is the exception to the rule that a 404 identifies a
missing resource. Every failure to read the restore-job store answers 404,
including a transient one, so NotFoundError from that call means “could not
produce this job” rather than “this job does not exist”.
Control flow keyed on it is therefore unsafe: giving up, deleting local state, or reporting the restore as gone can each be the wrong call on what was really a temporary store failure. Do not match on the message text either — a job whose target index was deleted also answers 404, with a different message than a genuinely missing job produces.
from pinecone.errors import NotFoundError
try:
job = pc.restore_jobs.describe(job_id="rj-abc123")
except NotFoundError:
# Possibly transient. Retry before concluding the job is gone.
...
Retries¶
The SDK retries transient failures automatically, on every HTTP method, before raising anything. Bulk operations additionally self-tune their concurrency down when a host pushes back.
What is retried, how long the backoff waits, what is configurable, and what to do in a multi-process fan-out: Retries and Resilience.
Timeouts¶
The default request timeout is 30 seconds. Pass timeout to the Pinecone
constructor to change the client-wide default:
pc = Pinecone(timeout=10.0)
Many methods also accept a per-request timeout that overrides the client default
for that call, and the bulk methods accept a total_timeout that bounds the whole
operation instead of one attempt. The distinction matters: timeout is the deadline
for a single HTTP attempt and each retry gets a fresh one, so a call can outlive its
timeout several times over. Retries and Resilience covers total_timeout.
A timeout that elapses raises PineconeTimeoutError, which also inherits from
Python’s built-in TimeoutError:
except TimeoutError:
# Catches PineconeTimeoutError as well
...
PineconeTimeoutError carries a response attribute, which is None for an
ordinary request timeout. On the bulk paths that raise instead of returning —
upsert_from_dataframe(on_error="raise"), and the gRPC upsert — it holds the
partial result, so response.failed_items tells you what to re-send rather than
leaving you to resubmit everything.