Errors
The app never sees a raw fetch failure or a provider’s error object. Plugins turn every problem into an IntegrationError with a
normalized code, and the app decides what to tell the user and whether to retry.
// Error normalization. UI code must never see raw fetch/provider exceptions.export type IntegrationErrorCode = | 'network-unreachable' | 'authentication-failed' | 'authentication-expired' | 'permission-denied' | 'not-found' | 'rate-limited' | 'unsupported-version' | 'invalid-response' | 'server-error' | 'unknown'
export interface IntegrationError { code: IntegrationErrorCode message: string providerMessage?: string statusCode?: number retryable: boolean details?: Record<string, unknown>}
export class IntegrationException extends Error implements IntegrationError { readonly code: IntegrationErrorCode readonly providerMessage?: string readonly statusCode?: number readonly retryable: boolean readonly details?: Record<string, unknown>
constructor(init: Partial<IntegrationError> & Pick<IntegrationError, 'code' | 'message'>) { super(init.message) this.name = 'IntegrationException' this.code = init.code this.providerMessage = init.providerMessage this.statusCode = init.statusCode this.retryable = init.retryable ?? isRetryable(init.code) this.details = init.details }}
export function isIntegrationError(value: unknown): value is IntegrationError { return ( value instanceof IntegrationException || (typeof value === 'object' && value !== null && typeof (value as IntegrationError).code === 'string' && typeof (value as IntegrationError).retryable === 'boolean') )}
export function isRetryable(code: IntegrationErrorCode): boolean { return code === 'network-unreachable' || code === 'rate-limited' || code === 'server-error'}
export function errorFromHttpStatus(status: number, providerMessage?: string): IntegrationException { const code: IntegrationErrorCode = status === 401 ? 'authentication-failed' : status === 403 ? 'permission-denied' : status === 404 ? 'not-found' : status === 429 ? 'rate-limited' : status >= 500 ? 'server-error' : 'unknown' return new IntegrationException({ code, message: `Request failed with status ${status}`, providerMessage, statusCode: status })}
/** Wrap anything thrown by transport/mapping code into a normalized error. */export function toIntegrationError(cause: unknown): IntegrationException { if (cause instanceof IntegrationException) return cause if (isIntegrationError(cause)) return new IntegrationException(cause) const providerMessage = cause instanceof Error ? cause.message : String(cause) if (cause instanceof TypeError) { return new IntegrationException({ code: 'network-unreachable', message: 'Could not reach the server', providerMessage }) } return new IntegrationException({ code: 'unknown', message: 'Unexpected integration failure', providerMessage })}Choosing a code
Section titled “Choosing a code”| Code | Use it when | Retryable |
|---|---|---|
network-unreachable |
The request never completed: offline, DNS, TLS, timeout. | yes |
authentication-failed |
Sign-in was rejected: wrong key, password or token. | no |
authentication-expired |
You were signed in and it stopped working, or you were never connected. The app will ask the user to sign in again. | no |
permission-denied |
Signed in, but not allowed to read this. | no |
not-found |
The container or item does not exist (any more). | no |
rate-limited |
The server asked you to slow down. | yes |
unsupported-version |
The server is a version the plugin cannot work with. | no |
invalid-response |
The server answered, but not in the shape the plugin expects. | no |
server-error |
The server failed (5xx). | yes |
unknown |
Anything else. Prefer a specific code when you can. | no |
Helpers
Section titled “Helpers”errorFromHttpStatus(status, providerMessage?)maps401,403,404,429and5xxto the codes above. A401maps toauthentication-failed; if you were already connected, throwauthentication-expiredyourself, as the tutorial does.toIntegrationError(cause)wraps anything thrown. ATypeError(whatfetchthrows when the network is down) becomesnetwork-unreachable; an existingIntegrationExceptionpasses through; everything else becomesunknown.new IntegrationException({ code, message, ... })builds one directly.retryabledefaults from the code.
Keep providerMessage for the remote’s own text: it is useful in logs and details, but the app shows its own wording.