Skip to content
UniBox

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.

@unibox-cam/integration-sdk · errors.ts
// 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 })
}
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
  • errorFromHttpStatus(status, providerMessage?) maps 401, 403, 404, 429 and 5xx to the codes above. A 401 maps to authentication-failed; if you were already connected, throw authentication-expired yourself, as the tutorial does.
  • toIntegrationError(cause) wraps anything thrown. A TypeError (what fetch throws when the network is down) becomes network-unreachable; an existing IntegrationException passes through; everything else becomes unknown.
  • new IntegrationException({ code, message, ... }) builds one directly. retryable defaults 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.