The typed-error model¶
When a connector fails to fetch, it raises a typed error from
parsimony.errors. Each typed error carries a provider attribute and a
default message written for the agent that called the connector. An agent can
branch on the error's type and attributes to decide what to do next, and
render the message verbatim with a single str(exc).
Operational errors vs programmer errors¶
The typed errors model operational failures — the things that go wrong when you talk to a remote provider: a bad key, a rate limit, an empty result, a server outage. These are normal outcomes a running agent must handle.
Programmer errors stay native. A bad argument shape, a type mismatch, a
schema-validation failure inside your own code — raise TypeError,
ValueError, or let a Pydantic ValidationError propagate. Those are bugs, not
operational states, and dressing them as connector errors hides them.
The one boundary case is a call-time argument check before any network call —
an empty series_id, an out-of-range page_size. That is operational (the agent
can fix it and retry) so it uses InvalidParameterError, not a bare
ValueError.
The error table¶
All import from parsimony.errors. The base is
ConnectorError(message, *, provider), but every subclass flips the argument
order so provider is the first positional — see the gotcha below.
| Error | Raise for | Notes |
|---|---|---|
UnauthorizedError(provider, env_var=...) |
401/403 from bad or missing credentials | env_var is keyword-only and names the variable the agent should set. |
PaymentRequiredError(provider) |
402, or a plan-tier restriction | Use even when the upstream status is a 401/403 whose body says the plan is the problem. |
RateLimitError(provider, retry_after, *, quota_exhausted=False) |
429 | retry_after is seconds-until-retry; quota_exhausted=True means the billing-period quota is gone. |
ProviderError(provider, status_code) |
5xx, 404, or timeout (408) | Carries the status_code. |
EmptyDataError(provider, query_params=...) |
HTTP 200 with zero rows | A valid outcome — recovery is to adjust params, so no "do not retry" directive. |
ParseError(provider, msg=None) |
HTTP 200 that cannot be parsed, or schema drift | The honest mapping for a 200 carrying an error body. |
InvalidParameterError(provider, message) |
A bad call-time argument, before any network call | message is required. |
CatalogNotFoundError(msg) |
A catalog bundle missing or unreachable | Takes the message first; provider defaults to "catalog". |
retry_after must be a duration¶
RateLimitError rejects a retry_after greater than 86,400 seconds — that
looks like a Unix epoch timestamp, not a delay. Pass seconds-until-retry
(60.0), never an absolute timestamp.
The agent-facing-message principle¶
For every typed subclass, the kernel-built default message is the canonical
agent-facing string. Each default embeds the right semantics and the right
agent-loop directive — DO NOT retry, pick a different connector, adjust
parameters — so a consumer renders it with one str(exc) call and the agent
knows what to do.
UnauthorizedError("fred", env_var="FRED_API_KEY") produces, roughly:
fred: API credentials missing or invalid — set the FRED_API_KEY env var (and ensure it is exported). DO NOT retry with different arguments.
You rarely pass message=. It is an escape hatch for context the kernel cannot
construct (an upstream error_code, say). If you override, you own the
agent-facing text: keep it free of URLs, tokens, and upstream-derived prose that
could carry credentials or a prompt-injection payload.
Branch on attributes, never on strings¶
The message is for the agent to read; it is not a stable contract for code to
parse. Branch on the error type and its attributes — provider,
status_code, retry_after, quota_exhausted, env_var, query_params —
never on substrings of the message. Messages are tuned for agents and will
change; attributes are the contract.
from parsimony.errors import ProviderError
try:
result = some_connector(...)
except ProviderError as exc:
if exc.status_code == 408:
... # upstream timeout — back off
elif 500 <= exc.status_code <= 599:
... # transient server error
The flipped-argument-order gotcha¶
ConnectorError, the base, takes the message first and provider as a
keyword-only argument:
But every subclass you actually raise puts provider first, positionally:
from parsimony.errors import (
EmptyDataError,
InvalidParameterError,
ProviderError,
RateLimitError,
UnauthorizedError,
)
UnauthorizedError("fred", env_var="FRED_API_KEY")
PaymentRequiredError("fred")
RateLimitError("fred", 60.0) # retry_after seconds
RateLimitError("fred", 0.0, quota_exhausted=True)
ProviderError("fred", 503) # status_code
EmptyDataError("fred", query_params={"series_id": sid})
ParseError("fred", "response missing 'observations'")
InvalidParameterError("fred", "series_id must be non-empty")
Writing EmptyDataError("no data", provider="fred") — base-style — puts the
message where provider is expected and is wrong. The first positional argument
to any subclass is the provider name.
What an agent does with each¶
The directive baked into each message tells the agent its next move. Two representative cases:
from parsimony.errors import EmptyDataError, RateLimitError
try:
result = provider_fetch(series_id="GDPC1")
except RateLimitError as exc:
if exc.quota_exhausted:
... # billing quota gone: switch connectors or wait for the next cycle
else:
... # burst limit: do not retry immediately; try elsewhere or wait
# exc.retry_after seconds
except EmptyDataError as exc:
... # a clean 200 with no rows: adjust exc.query_params (a wider date
# window, a different id) and retry, or try a different connector
RateLimitError— distinguish the two cases byquota_exhausted. A burst limit (False) is recoverable afterretry_afterseconds; a quota (True) is not recoverable this billing period, so the agent should pivot.EmptyDataError— not a failure of the connector, a failure of the query. The agent should adjust parameters usingquery_paramsfor context, or move on. There is deliberately no "do not retry" directive.
See the connector contract for where these errors are raised, and using connectors for handling them on the consumer side.