Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.privacyboost.io/llms.txt

Use this file to discover all available pages before exploring further.

Error Handling Patterns

This page covers advanced error handling — classification, telemetry, structured logging, and circuit breakers. For the basics (try/catch, retry, re-auth, user-friendly messages), see the Error Handling guide.

Error Classification

SdkError is a tagged union — every variant has a stable tag you can read at runtime. Unlike the iOS / Android SDKs (which ship a category extension in their Defaults libraries), the React Native binding leaves classification to you. This three-line helper mirrors the categories used on the other platforms:
import { SdkError, SdkError_Tags } from '@sunnyside-io/privacy-boost-react-native';

export type ErrorCategory =
  | 'Auth'
  | 'Network'
  | 'Validation'
  | 'Permission'
  | 'RateLimit'
  | 'Server'
  | 'Internal';

export function categoryOf(err: unknown): ErrorCategory {
  if (!isSdkError(err)) return 'Internal';
  const tag = (err as { tag?: string }).tag;
  switch (tag) {
    case SdkError_Tags.NotAuthenticated:
    case SdkError_Tags.AuthenticationFailed:
    case SdkError_Tags.AuthServerError:
      return 'Auth';
    case SdkError_Tags.NetworkError:
      return 'Network';
    case SdkError_Tags.InvalidConfig:
    case SdkError_Tags.ConfigError:
    case SdkError_Tags.InvalidAddress:
    case SdkError_Tags.InvalidAmount:
    case SdkError_Tags.InvalidInput:
    case SdkError_Tags.InsufficientBalance:
      return 'Validation';
    case SdkError_Tags.Forbidden:
    case SdkError_Tags.SignatureRejected:
      return 'Permission';
    case SdkError_Tags.RateLimited:
      return 'RateLimit';
    case SdkError_Tags.ShieldError:
    case SdkError_Tags.TransferError:
    case SdkError_Tags.NoteError:
    case SdkError_Tags.MerkleError:
    case SdkError_Tags.ApiError:
    case SdkError_Tags.ResourceNotFound:
      return 'Server';
    default:
      return 'Internal';
  }
}

export function isRetryable(err: unknown): boolean {
  if (!isSdkError(err)) return false;
  const tag = (err as { tag?: string }).tag;
  if (tag === SdkError_Tags.NetworkError || tag === SdkError_Tags.RateLimited) {
    return true;
  }
  if (tag === SdkError_Tags.ApiError) {
    return Boolean((err as { retryable?: boolean }).retryable);
  }
  return false;
}

function isSdkError(err: unknown): boolean {
  // Every SdkError variant is constructable as a class — instanceOf checks below.
  return (
    SdkError.NotConnected.instanceOf(err) ||
    SdkError.NotAuthenticated.instanceOf(err) ||
    SdkError.NetworkError.instanceOf(err) ||
    SdkError.WalletError.instanceOf(err) ||
    SdkError.InsufficientBalance.instanceOf(err) ||
    SdkError.InvalidAddress.instanceOf(err) ||
    SdkError.InvalidAmount.instanceOf(err) ||
    SdkError.SerializationError.instanceOf(err) ||
    SdkError.AuthServerError.instanceOf(err) ||
    SdkError.ShieldError.instanceOf(err) ||
    SdkError.TransferError.instanceOf(err) ||
    SdkError.NoteError.instanceOf(err) ||
    SdkError.MerkleError.instanceOf(err) ||
    SdkError.ApiError.instanceOf(err) ||
    SdkError.RateLimited.instanceOf(err) ||
    SdkError.Forbidden.instanceOf(err) ||
    SdkError.ResourceNotFound.instanceOf(err) ||
    SdkError.InternalError.instanceOf(err) ||
    SdkError.SignatureRejected.instanceOf(err)
  );
}
CategoryMeaningRecommended UIRetry?
AuthSession/auth flow failedRe-prompt sign-inNo (but re-auth and try once)
NetworkConnectivity / DNS / timeout”Check your connection”Yes, with backoff
ValidationInput is malformedInline form errorNo
PermissionUser cancelled / forbiddenSilent dismiss or contact supportNo
RateLimitToo many requestsWait + auto-retryYes, after retryAfterMs
ServerBackend / proof / merkle failureGeneric error toastSometimes (check isRetryable)
InternalBug or invariant violation”Something went wrong” + reportNo

Structured Logging

Log SDK errors with stable fields so they’re queryable in your log pipeline (Logcat, Console, Datadog, etc.).
import { SdkError, SdkError_Tags } from '@sunnyside-io/privacy-boost-react-native';

function variantTag(err: unknown): string {
  const tag = (err as { tag?: string }).tag;
  if (!tag) return 'Other';
  if (tag === SdkError_Tags.ApiError) return `ApiError.${(err as { code?: string }).code ?? '?'}`;
  if (tag === SdkError_Tags.ShieldError) return `ShieldError.${(err as { code?: string }).code ?? '?'}`;
  if (tag === SdkError_Tags.TransferError) return `TransferError.${(err as { code?: string }).code ?? '?'}`;
  return tag;
}

export function logError(operation: string, err: unknown) {
  const category = categoryOf(err);
  const variant  = variantTag(err);
  const retry    = isRetryable(err);

  console.error(`[PB] [${operation}] category=${category} retryable=${retry} variant=${variant}`, err);
}
If your message field could include PII, scrub or omit it from logs.

Error Reporting Integration

Sentry

import * as Sentry from '@sentry/react-native';

export function reportSdk(err: unknown, operation: string) {
  Sentry.withScope((scope) => {
    scope.setTag('pb.operation', operation);
    scope.setTag('pb.category', categoryOf(err));
    scope.setTag('pb.retryable', String(isRetryable(err)));
    scope.setTag('pb.variant', variantTag(err));
    Sentry.captureException(err);
  });
}
The pb.category tag lets you build dashboards that group errors by class — “auth-class errors are spiking” is more actionable than “twelve different variants are spiking.”

Filter Out Noise

User cancellations (SignatureRejected) and validation errors aren’t bugs — don’t ship them to your error tracker:
export function shouldReport(err: unknown): boolean {
  const cat = categoryOf(err);
  return cat !== 'Permission' && cat !== 'Validation';
}

Operation-Scoped Wrapping

Wrap each meaningful SDK call so logging, reporting, and retry sit in one place.
export async function runSdk<T>(
  operation: string,
  block: () => Promise<T>,
): Promise<T> {
  try {
    return await block();
  } catch (err) {
    logError(operation, err);
    if (shouldReport(err)) reportSdk(err, operation);
    throw err;
  }
}

// Usage
const result = await runSdk('shield', () =>
  sdk.shield(token, amount),
);

Circuit Breaker

If the backend is degraded, hammering it with retries makes things worse. Wrap operations in a circuit breaker that trips after consecutive server-class failures.
type BreakerState =
  | { kind: 'closed' }
  | { kind: 'open'; until: number }
  | { kind: 'halfOpen' };

class CircuitBreaker {
  private state: BreakerState = { kind: 'closed' };
  private consecutive = 0;
  constructor(
    private readonly threshold = 5,
    private readonly cooldownMs = 60_000,
  ) {}

  canPass(): boolean {
    if (this.state.kind === 'closed' || this.state.kind === 'halfOpen') return true;
    if (Date.now() >= this.state.until) {
      this.state = { kind: 'halfOpen' };
      return true;
    }
    return false;
  }

  recordSuccess() {
    this.consecutive = 0;
    this.state = { kind: 'closed' };
  }

  recordFailure(err: unknown) {
    const cat = categoryOf(err);
    if (cat !== 'Server' && cat !== 'Network') return;
    this.consecutive++;
    if (this.consecutive >= this.threshold) {
      this.state = { kind: 'open', until: Date.now() + this.cooldownMs };
    }
  }
}

const breaker = new CircuitBreaker();

export async function guarded<T>(block: () => Promise<T>): Promise<T> {
  if (!breaker.canPass()) {
    throw new Error('circuit open');
  }
  try {
    const result = await block();
    breaker.recordSuccess();
    return result;
  } catch (err) {
    breaker.recordFailure(err);
    throw err;
  }
}
Auth/validation/permission failures should not trip the breaker — they’re user-class problems, not service degradation. The classification by categoryOf makes that distinction trivial.

Telemetry: Operation Latency by Outcome

Pair errors with timings — slow successes are interesting too.
interface OpMetric {
  operation: string;
  durationMs: number;
  outcome: string;
}

export async function instrument<T>(
  operation: string,
  sink: (m: OpMetric) => void,
  block: () => Promise<T>,
): Promise<T> {
  const start = performance.now();
  try {
    const result = await block();
    sink({ operation, durationMs: performance.now() - start, outcome: 'success' });
    return result;
  } catch (err) {
    sink({ operation, durationMs: performance.now() - start, outcome: categoryOf(err) });
    throw err;
  }
}
Forward OpMetric to Sentry Performance, Datadog RUM, Firebase Performance, or your own analytics pipeline.

Best Practices

  1. Branch on categoryOf(err), not on individual variants — UI logic stays small and stable across SDK upgrades.
  2. Filter user-class errors out of error reportersPermission and Validation are noise.
  3. Tag every report with pb.category and pb.variant — makes dashboards actually useful.
  4. Trip circuit breakers only on Server and Network — never on auth or validation.
  5. Pair errors with timings — a slow success that turns into a timeout is worth catching upstream.

Next Steps