Skip to content

Error Handling

Errors surface through a layered ThetaDataError hierarchy so callers can narrow an except clause to the specific failure they want to recover from. The Rust core defines the enum once; each SDK exposes it in its language idiom.

Exception hierarchy

ThetaDataError (base)
├── AuthError              -- bad credentials, expired session
├── SubscriptionError      -- endpoint requires a higher tier
├── RateLimitError         -- TooManyRequests (code 12)
├── EndpointNotFoundError  -- server returned NotFound for a known endpoint
├── SchemaMismatchError    -- decoder cannot unpack the response
├── NetworkError           -- connect / TLS / stream failures
└── TimeoutError           -- deadline exceeded
rust
use thetadatadx::{ThetaDataDxClient, Error};

match client.option_history_greeks_all("SPY", "20240419", "500", "C", "20240101", "20240301").await {
    Ok(ticks) => process(ticks),
    Err(Error::RateLimited { wait_ms, .. }) => {
        tokio::time::sleep(std::time::Duration::from_millis(wait_ms)).await;
        // retry
    }
    Err(Error::Subscription { endpoint, required_tier }) => {
        eprintln!("{endpoint} requires {required_tier}");
    }
    Err(Error::Auth(_)) => refresh_credentials(),
    Err(err) => return Err(err),
}
python
from thetadatadx import ThetaDataError, AuthError, SubscriptionError, RateLimitError

try:
    ticks = tdx.option_history_greeks_all("SPY", "20240419", "500", "C",
                                          "20240101", "20240301")
except RateLimitError as e:
    time.sleep(e.wait_seconds)
    # retry
except SubscriptionError as e:
    print(f"Endpoint {e.endpoint} requires {e.required_tier}")
except AuthError:
    refresh_credentials()
except ThetaDataError as e:
    logger.exception("unexpected SDK failure")
    raise
typescript
import {
    ThetaDataError, AuthError, SubscriptionError, RateLimitError,
} from 'thetadatadx';

try {
    const ticks = tdx.optionHistoryGreeksAll('SPY', '20240419', '500', 'C',
                                             '20240101', '20240301');
} catch (e) {
    if (e instanceof RateLimitError) {
        await new Promise(r => setTimeout(r, e.waitMs));
        // retry
    } else if (e instanceof AuthError) {
        refreshCredentials();
    } else {
        throw e;
    }
}
cpp
try {
    auto ticks = client.option_history_greeks_all("SPY", "20240419", "500", "C",
                                                  "20240101", "20240301");
} catch (const tdx::RateLimitError& e) {
    std::this_thread::sleep_for(std::chrono::milliseconds(e.wait_ms()));
    // retry
} catch (const tdx::AuthError&) {
    refresh_credentials();
} catch (const tdx::ThetaDataError& e) {
    std::cerr << e.what() << std::endl;
    throw;
}

Retry policy

The SDK does not retry historical calls automatically. Callers decide — network blips, rate limits, and timeouts are explicit signals you may want to route through different code paths. The concrete knobs:

FailureAuto-handledCaller action
AuthError — session expiredNorefresh_credentials() and retry
AuthError — bad credentialsNoSurface to user; retrying will not help
RateLimitError (TooManyRequests)NoSleep wait_ms (server-provided), then retry
NetworkError — connection resetNoShort backoff then retry
TimeoutErrorNoIncrease timeout or retry
SubscriptionErrorNoWrong tier; retrying will not help
SchemaMismatchErrorNoFile a bug (decoder drift)

A recommended shape — tenacity for Python, tokio-retry for Rust, a handwritten loop for Go/C++/TypeScript — is:

python
from tenacity import retry, retry_if_exception_type, wait_exponential, stop_after_attempt
from thetadatadx import RateLimitError, NetworkError, TimeoutError

@retry(
    retry=retry_if_exception_type((RateLimitError, NetworkError, TimeoutError)),
    wait=wait_exponential(multiplier=1, min=2, max=60),
    stop=stop_after_attempt(5),
)
def pull_chain(tdx, symbol, exp):
    return tdx.option_snapshot_quote(symbol, exp, "*", "both")

Session auto-refresh

The client owns a session UUID returned by ThetaData's Nexus auth endpoint. It attaches the UUID to every gRPC call and every FPSS frame. On an Unauthenticated gRPC status, the client re-runs the auth exchange once automatically and retries the call with the fresh UUID before surfacing AuthError to the caller. This covers the common case of a session expiring mid-pipeline without forcing the caller to wrap every endpoint call.

Re-auth is one-shot: if the fresh UUID also fails, the underlying cause is a credential problem, not a session lifecycle one, and the error propagates.

Timeouts

Every timeout is configurable through DirectConfig / Config. Defaults aimed at a well-behaved production deployment:

TimeoutDefaultConfigurable as
Nexus HTTP connect5,000 msnexus_connect_timeout_ms
Nexus HTTP request10,000 msnexus_request_timeout_ms
MDDS gRPC keepalive30,000 msmdds_keepalive_secs
FPSS connect2,000 msfpss_connect_timeout_ms
FPSS read10,000 msfpss_timeout_ms
FPSS ping100 msfpss_ping_interval_ms
Reconnect (normal)2,000 msreconnect_wait_ms
Reconnect (rate-limited)130,000 msreconnect_wait_rate_limited_ms

See Configuration for the full struct.

Next

  • Quick Start — install + first call + first stream, tabbed per language
  • DataFrames — Arrow / Polars / Pandas output with the zero-copy scope

Released under the Apache-2.0 License.