Standard Error Handling Patterns in SDKs Across Languages

Standard error handling patterns in SDKs use exceptions, error return values, and Result types to deliver idiomatic failure handling and simplify debugging.

Jump to section

Jump to section

Jump to section

When your API returns a 500 error, your Python SDK should raise a `ServerError` exception, your Go SDK should return an `error` value, and your TypeScript SDK should return a `Result` type. Each language has its own idiomatic way of handling failures, and a well-designed SDK respects these conventions rather than forcing a one-size-fits-all approach.

This article covers the three core error handling patterns used across modern SDKs—exceptions, error return values, and Result types—along with essential retry logic, timeout configuration, and structured error data that makes debugging straightforward for your API users.
Consistent, cross-language error handling is the bedrock of a trustworthy developer experience. By standardizing how your SDKs report issues, you reduce debugging time for your users and build confidence in your API, a process Stainless automates to feel hand-crafted in every language.

What are the core error handling patterns for SDKs?

The core error handling patterns for SDKs are typically exceptions, error return values, and Result types. Unlike general application errors, SDK errors must account for network failures, API-specific issues, and provide clear debugging information for developers who did not write the underlying code. Choosing the right pattern for each language is crucial for creating an idiomatic and predictable developer experience.

A good SDK does not just wrap API calls; it translates API behavior into the patterns a developer expects in their chosen language. This is why your API isn't finished until the SDK ships. The goal is to make an API error feel like a native language error. This means your Python SDK should raise Pythonic exceptions, while your Go SDK returns an error value.

Here are the main approaches you will see:

  • Exceptions: Code execution is interrupted and jumps to the nearest error handler. Common in Java, Python, and C#.

  • Error Return Values: Functions return a special value, often alongside the result in a tuple, to indicate an error occurred. This is the standard in Go.

  • Result Types: Functions return a wrapper object that contains either a success value or an error. This pattern is gaining popularity in TypeScript, Rust, and Kotlin for its type-safety.

When generating SDKs from your OpenAPI spec, Stainless automatically maps HTTP status codes and error responses to the correct idiomatic pattern for each language: in Python, methods throw custom exceptions like NotFoundError or RateLimitError; in Go, calls return a (value, error) tuple; in TypeScript, by default non-2xx responses are thrown as errors, or you can configure a discriminated Result<T, APIError> union to handle them as values. First, you'll need to create an OpenAPI spec that accurately represents your API.

How do error return values work?

The error return value pattern forces the calling code to explicitly check for an error before using the result. This avoids silent failures and makes the error path a first-class part of your code's logic. The most well-known example is Go's (value, error) tuple.

Here’s how you might handle a 404 Not Found using this pattern in Go:

user, err := client.Users.Retrieve("user-123")
if err != nil {
    var rateLimitErr *RateLimitError
    if errors.As(err, &rateLimitErr) {
        log.Printf("Rate limited. Please retry after: %v", rateLimitErr.RetryAfter)
    } else {
        return fmt.Errorf("unexpected error retrieving user: %w", err)
    }
}
fmt.Println("Retrieved user:", user.Name

This pattern is native to Go. It makes error handling very explicit: you cannot accidentally ignore an error, because you have to check the err return value. While it can feel more verbose, it prevents entire classes of bugs related to unhandled failures.

When should SDKs throw exceptions?

Exceptions should be reserved for unexpected or unrecoverable errors. In languages like Python (and optionally in TypeScript), generated SDK methods will throw exceptions on non-2xx HTTP status codes. When an SDK throws an exception, it signals that something went wrong that the immediate calling code likely cannot handle, such as a network failure, a 500-level server bug, or a critical configuration issue like a missing API key.

A well-designed SDK provides a hierarchy of custom exception types. This allows developers to write targeted catch blocks for specific errors they can handle, like rate limits, while letting other unexpected errors propagate.

A typical hierarchy looks like this:

  1. APIError: A base class for all errors originating from the SDK.

  2. APIConnectionError: For when the SDK cannot reach the API server.

  3. RateLimitError: For 429 Too Many Requests responses.

  4. AuthenticationError: For 401 Unauthorized or 403 Forbidden responses.

Here is how a developer might use this in Python:

try:
    user = client.users.retrieve("user-123")
except NotFoundError:
    print("User not found.")
except RateLimitError as e:
    print(f"Rate limited. Please wait. Request ID: {e.request_id}")
except APIConnectionError:
    print("Could not connect to the API. Please check your network.")

This structure gives developers fine-grained control. They can gracefully handle predictable issues like "not found" or rate limits while letting more severe, unexpected errors bubble up.

How do Result types create predictable SDK errors?

Result types, also known as Either types, are a hybrid approach that combines the explicitness of return values with the strong typing of an object. A function that can fail returns a Result object which contains either a success value or an error value, but never both. This is enforced by the type system at compile time.

This pattern prevents you from accessing a result that does not exist, eliminating null pointer exceptions and forcing you to handle the error case. It is particularly powerful in languages with strong static analysis like TypeScript, Rust, and Swift. In TypeScript, you can opt into this pattern by configuring your SDK generator to return a discriminated union for error-prone endpoints.

Using a Result type looks like this in TypeScript:

type Result<T, E> =
  | { success: true; value: T }
  | { success: false; error: E };

const result: Result<User, APIError> = await client.users.retrieve("user-123");

if (result.success) {
    console.log("Retrieved user:", result.value.name);
} else {
    console.error("Error:", result.error.message);
}

The compiler understands that you can only access value inside the if (result.success) block, making your code safer and easier to reason about. While powerful, this can add a layer of ceremony, so it is best used when the explicitness adds significant safety.

What retry patterns belong in every SDK?

Network requests are inherently unreliable. A robust SDK should automatically retry transient failures so that the developer using it does not have to. This is a core responsibility of an SDK and should be enabled by default for idempotent requests.

1. Exponential backoff

When a request fails, you should not retry it immediately. Exponential backoff with jitter is the standard algorithm for this. It works by progressively increasing the wait time between retries, with a small amount of randomness (jitter) to prevent clients from retrying in lockstep and overwhelming the server.

You can typically configure the number of retries and the maximum delay. For example, in a stainless.yml config, you might set:

client_settings:
  default_retries:
    max_retries: 3
    max_delay_seconds: 30

This tells the generated SDK to retry up to 3 times, waiting longer each time, but never more than 30 seconds.

2. Idempotency keys

Retrying GET requests is safe, but retrying POST requests can be dangerous. What if the first request succeeded but the response was lost? Retrying it would create a duplicate resource.

Idempotency keys solve this. The SDK generates a unique key for each mutating request and sends it in a header, like Idempotency-Key. If the server sees the same key twice, it knows it is a retry and can safely return the original response without performing the action again.

3. Timeout configuration

Timeouts prevent a request from hanging indefinitely. It is important to distinguish between different types of timeouts:

  • Connection Timeout: How long to wait to establish a connection with the server.

  • Request Timeout: How long to wait for the server to return a full response after the connection is made.

A good SDK allows developers to configure these timeouts both globally on the client and on a per-request basis for long-running operations.

SDK error handling best practices

Building a great error handling experience goes beyond just picking a pattern. It is about providing developers with the context they need to debug issues quickly and effectively.

Best Practice

How Stainless Helps

Actionable Error Messages

Error messages should explain what went wrong and suggest a fix.

Structured Error Data

Include a request ID, status code, and error type in a machine-readable format.

Consistent Error Codes

Use a consistent set of error codes across your API and all SDKs.

Respect Retry-After

Obey the Retry-After header for 429 and 503 responses.

Customizable Retry Logic

Allow users to override default retry behavior for specific endpoints.

Clear Timeout Semantics

Document and provide options for connection, read, and write timeouts.

By baking these practices into your SDKs from the start, you create a developer experience that feels solid, reliable, and easy to work with. When you need specialized error handling, you can add custom code that persists through regeneration.

Frequently asked questions about SDK error handling

How do I keep error handling consistent across all language SDKs?

Use a tool that generates SDKs from a single source of truth, like an OpenAPI spec and a generator config. The Stainless SDK generator handles this automatically across multiple languages.

Should a 404 return null or throw?

If "not found" is an expected outcome (e.g., findUserByEmail), return null or an empty optional. If it is an unexpected error (e.g., trying to update a non-existent resource), throw a NotFoundError.

How do I evolve error formats without breaking users?

Add new, optional fields to your error objects instead of removing or renaming old ones. Use semantic versioning in your SDK releases to clearly signal any breaking changes. Apply backwards-compatible patterns like making Java enums forwards compatible to minimize version conflicts.

What is the difference between connection timeout and request timeout?

A connection timeout applies only to establishing the initial network connection. A request timeout applies to the entire API call, from sending the request to receiving the complete response.

How should SDKs expose rate limit information?

SDKs should automatically parse standard headers like Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining. This data should be available on the error object for programmatic access.

Ready to ship SDKs with best-in-class error handling built-in? Get started for free.