Skip to content
ub3r.io

SDK Design · Chapter 2 · 12 min

Errors as values

Typed error hierarchies, result types and making the retryable/terminal split obvious.

Structural error handling

A communication failure can happen for many reasons, from transient network blips to permanent validation errors. An SDK that throws generic errors forces developers to rely on fragile string matching to determine the cause of a failure. By providing a structured error hierarchy, the ub3r SDK allows callers to distinguish between retryable and terminal errors programmatically. Each error object includes the HTTP status code, a machine-readable error code, and a unique request identifier for support requests.

Diff· Structured, actionable SDK errors
if (!res.ok) {
− throw new Error("Request failed");
}

The Result pattern

In high-integrity systems, returning a Result type is often superior to throwing exceptions for expected failure modes. This pattern forces the caller to explicitly check whether an operation succeeded before accessing the returned value. By making the error path visible in the code, the ub3r library encourages developers to handle edge cases like rate limits or invalid numbers upfront. This leads to more robust applications that don't suffer from unhandled promise rejections in production.

The ub3r library offers a try variant for most methods to satisfy different architectural preferences. While some teams prefer the brevity of try-catch blocks, others value the type-safety of result objects. Providing both ensures that the SDK remains ergonomic across a wide variety of project styles. Consistency in these patterns across the entire library makes it easy for teams to adopt a unified error handling strategy.

Centralized error handling logic can be implemented by wrapping the SDK's client methods in a custom middleware layer. This allows teams to inject common logging, metrics, and alerting logic into every API call without duplicating code across the codebase. For example, you can automatically increment a Prometheus counter every time a specific error code is encountered. This high-level telemetry provides a clear view of the platform's health from the perspective of your application.

Categorizing failures

We categorize errors into four main groups: authentication, validation, rate limits, and internal platform issues. This categorization helps developers implement specific logic for each scenario, such as clearing a cache on authentication failure or backing off on a 429. By attaching a retryable boolean to each error, we simplify the logic for custom retry loops. This removes the guesswork from building resilient integrations that can withstand temporary disruptions.

Providing a clear path to resolution in error messages is a hallmark of a great developer experience. Each ub3r error includes a link property pointing to the relevant section of the documentation. This small detail saves developers minutes of searching and helps them fix issues faster. When an error occurs, the SDK should be a helpful guide rather than a cryptic gatekeeper.

ts
const result = await client.messages.tryCreate({ to: "invalid", text: "hi" });
if (!result.ok) {
  console.error(`Failed with code ${result.error.code}. Request ID: ${result.error.requestId}`);
  if (result.error.retryable) {
    // Re-queue for later
  }
}

Discussion (4)

SO
Samuel Okafor · 2026-10-09

The callout about alerting on queue age instead of depth changed how we page people. Thank you.

LF
Lena Fischer · 2026-10-12

Same experience here. Per-route metrics were the only way we found it.

DO
Daniela Ortiz · 2026-10-01

The distinction between 'sent' and 'delivered' finally clicked for me here. We had dashboards treating them as the same thing for a year.

ML
Marcus Lindqvist · 2026-10-02

Would love a follow-up on how this behaves under carrier throttling. We see very different numbers per route.