CloudPDF
DocsPricing
Start building

Async & errors

Two primitives run through the entire engine: AbortablePromise for every async call, and EngineError for every failure. Both are re-exported from @cloudpdf/engine.

AbortablePromise#

Every async method returns an AbortablePromise. It’s a real Promise subclass, so you await it like anything else — but it also lets you cancel the underlying work and (optionally) observe progress.

import { AbortablePromise } from '@cloudpdf/engine';
 
const pending = doc.page(pon).render.image({ viewport: { kind: 'width', width: 1600 } });
 
// Later — the user scrolled away before it finished:
pending.abort();
  • abort(reason?) rejects the promise immediately with an AbortError (wrapping reason if provided) and fires the internal AbortSignal so the in-flight fetch is cancelled. Aborting an already-settled promise is a no-op.
  • signal is the AbortSignal for the operation, if you need to chain cancellation.
  • onProgress(cb) subscribes to progress events and returns an unsubscribe function (operations that don’t emit progress simply never call it).
const unsub = pending.onProgress((p) => updateBar(p));
const image = await pending;
unsub();

Because AbortablePromise is a Promise subclass, any .then()/.catch()/await returns a plain promise — only the original object exposes .abort(). Keep a reference to it if you intend to cancel.

EngineError#

Failures reject with an EngineError carrying a stable code from EngineErrorCode. Use the code, not the message, for control flow.

import { EngineError, EngineErrorCode } from '@cloudpdf/engine';
 
try {
  const text = await doc.page(pon).text.read();
} catch (err) {
  if (EngineError.is(err, EngineErrorCode.Forbidden)) {
    showUpgradePrompt();
  } else if (EngineError.is(err, EngineErrorCode.Aborted)) {
    // user cancelled — ignore
  } else {
    throw err;
  }
}

Common codes#

HTTP responses from the server map onto these codes:

CodeTypical cause
UnauthenticatedMissing/invalid token (HTTP 401).
ForbiddenToken lacks the required scope (HTTP 403).
NotFoundDocument, page, or annotation doesn’t exist (HTTP 404).
DocPasswordRequired / DocPasswordIncorrectEncrypted document needs a (correct) password.
InvalidReferenceA stale or out-of-range AnnotationRef (e.g. an index ref with an old revision).
WeakAnnotationSessionConflictA structural annotation edit raced another client (HTTP 409).
NetworkThe fetch itself failed (offline, DNS, TLS).
AbortedThe operation was cancelled via abort().
InvalidArgMalformed input (e.g. an unsupported OpenInput.kind).
RuntimeUnavailableThe engine was already destroyed, or a browser API (object URLs) is unavailable.
WireFormatThe server returned an unexpected response shape.

Don’t pattern-match on error messages — they’re for humans and may change. Branch on err.code (or EngineError.is(err, code)), which is part of the stable contract.

Automatic backpressure retries#

Two server conditions are retried for you, inside the transport: an overload shed (503 with code EngineBusy) and a request that raced an engine restart (EngineRestarting). Both are safe to retry by construction — they mean the server applied nothing — so the client waits out the server’s Retry-After hint (with jitter, respecting your AbortSignal) and retries up to twice before surfacing the error. Reads and mutations get this; any other failure, including a bare 503 from a proxy in front of the server, surfaces immediately as usual.

You never have to handle these yourself, but you can observe them:

const engine = cloudEngine({
  baseUrl: 'https://engine.example.com',
  token,
  onRetry: ({ path, code, attempt, waitMs }) => {
    console.debug(`retrying ${path} after ${code} (attempt ${attempt}, waited ${waitMs}ms)`);
  },
});

Putting it together#

const pending = doc.page(pon).render.image({ viewport: { kind: 'width', width: 1200 } });
 
try {
  const image = await pending;
  const { url, revoke } = await image.objectUrl();
  show(url, revoke);
} catch (err) {
  if (EngineError.is(err, EngineErrorCode.Aborted)) return; // cancelled
  reportError(err);
}