# Errors & retries

> Exceptions STX.Sdk throws, what they mean, and how the SDK retries transient failures.

Source: https://docs.stxapp.io/sdks/csharp/errors-and-retries/

Every public service method throws a typed exception for the error conditions you care about. Network blips are auto-retried via Polly before surfacing.

## Exceptions you'll see

| Exception | When | Recovery |
|---|---|---|
| `STXWrongCredentialsException` | Email or password rejected at login. Legacy path only | Prompt for fresh credentials |
| `STXSessionExpiredException` | JWT and refresh token both expired. Legacy path only | Re-authenticate |
| `STXTokenExpiredException` | JWT expired but refresh token is still valid | Call `STXTokenService.RefreshTokenAsync`, or rely on `keepSessionAlive: true` |
| `STXCancelOnDisconnectNotEnabledException` | `ConfirmOrderAsync(cancelOnDisconnect: true)` called before joining `STXActiveOrdersChannel` | Join the channel first, or pass `cancelOnDisconnect: false` |
| `STXGeoComplyException` | Geo-compliance check failed | User is outside a permitted jurisdiction; surface to your UI |
| `GraphQLHttpRequestException` | Non-200 HTTP response from the GraphQL endpoint | Usually transient; retries handled internally (see below) |

All STX-prefixed exceptions live in `STX.Sdk.Exceptions`.

## Example: handle auth failures

```csharp
try
{
    var me = await identity.GetMeAsync();
}
catch (STXRequestFailedException ex)
{
    // A rejected signature lands here. The usual cause is clock skew: the server
    // allows 30 seconds, so check NTP before suspecting the key.
    _logger.LogError(ex, "Could not authenticate");
}
catch (STXGeoComplyException ex)
{
    // User-facing: "Trading isn't available in your region."
    _logger.LogWarning(ex, "Geo block");
}
```

## Example: refresh on expired token (legacy path)

Only relevant to email and password. API keys have no token to expire, so none of this is needed.

When not using `keepSessionAlive: true`, wrap trading calls with a token-refresh retry:

```csharp
async Task<T> WithSession<T>(Func<Task<T>> call)
{
    try { return await call(); }
    catch (STXTokenExpiredException)
    {
        await _tokens.RefreshTokenAsync();
        return await call();
    }
    catch (STXSessionExpiredException)
    {
        await _login.LoginAsync(_email, _password);
        return await call();
    }
}

var order = await WithSession(() =>
    _orders.ConfirmOrderAsync(price, qty, marketId, action, type));
```

## Transient retries (automatic)

Retry is applied per call site, not by the transport, so it covers **some** methods and not others. As of 1.6.0 it covers every order mutation, all of `STXMarketService`, `STXIdentityService`, terms-and-conditions acceptance, and login and token refresh, which previously had no retry at all.

These still call the GraphQL client directly and are **not** retried:

| Not retried |
|---|
| `STXOrderService.GetMyOrdersAsync` |
| `STXTradeService.GetMyTradesAsync`, `GetMyTradesForOrderAsync` |
| `STXSettlementService.GetMySettlementsAsync` |
| `STXEventService.GetEventInfosAsync` |
| `STXTermsAndConditionsService.GetTermsAndConditionsAsync` |
| `STXGeoFencingLicenseService` |

If a brief outage during one of those matters to you, wrap it yourself. Everywhere else, don't add your own retry on top: double-retry amplifies an outage rather than absorbing it.

Where it does apply the policy is `RequestNumberOfRetry = 3`, meaning **3 retries after the initial call, so 4 requests in total**, with exponential backoff plus jitter: roughly 200ms, 400ms and 800ms, so about 1.4s of sleep on top of the per-attempt HTTP timeout. Size any outer deadline or circuit breaker against that, not against a single request.

The policy retries anything that could plausibly succeed on a second attempt:

| Retried | Not retried |
|---|---|
| `HttpRequestException` and other network failures | `STXWrongCredentialsException` |
| 5xx from the GraphQL endpoint | `STXUnauthorizedException`, `STXSessionExpiredException`, `STXTokenExpiredException` |
| **404**, which is what an ingress returns mid-deployment | `STXBadQueryObjectException`, `STXBadArgumentException`, `ArgumentException` |
| Timeouts (`TaskCanceledException` wrapping `TimeoutException`) | **429**, and genuine cancellation |

Credential errors are excluded deliberately, including a bare HTTP 401 or 403. Repeating a rejected credential cannot succeed, and on the legacy background re-login path it actively causes harm by driving the account towards a lockout.

If a failure persists past the built-in retries, `STXRequestFailedException` surfaces with the original exception as its `InnerException`, so the underlying cause (an HTTP status, a DNS failure, a timeout) is still available:

```csharp
catch (STXRequestFailedException ex) when (ex.InnerException is GraphQLHttpRequestException http)
{
    _logger.LogWarning("GraphQL returned {Status}", http.StatusCode);
}
```

## Rate limits

The exchange rate-limits by IP + account. Bursty order placement can return an HTTP 429. The SDK does **not** retry 429s (retrying is what the limiter is pushing back against). If you see them, lower your request rate or batch via `ConfirmOrdersAsync`.

## Debugging

Enable `Debug` logging to see full GraphQL request/response bodies:

```csharp
services.AddLogging(builder =>
    builder.SetMinimumLevel(LogLevel.Debug).AddConsole());
```

Every call is logged with its operation name, variables, and elapsed time. That's usually enough to tell whether an error was transient or a schema mismatch.
