# Errors

Every failure the SDK reports is an `SDKError` with a stable code you can switch on. Nothing is thrown as a bare string.

## SDKError

| Field | Type | Description |
| --- | --- | --- |
| `code` | `ErrorCodeValue` | Machine-readable error code, one of the `ErrorCode` values. Switch on this; never match on `message`. |
| `message` | `string` | Human-readable description, for logs rather than end users. Not stable API. |
| `fatal` | `boolean` | Whether the SDK itself is unusable after this error. `false` by default; `new SDKError(code, message, fatal = false)` is public, for test doubles. |
| `cause?` | `unknown` | What failed underneath, where there is something: `activateTool()` rejects with `UNKNOWN_ERROR` carrying the tool's own failure as its `cause`. |

`fatal: true` means the SDK itself is unusable: the engine failed to load (`ENGINE_LOAD_FAILED`) or the SDK stopped on an unrecoverable fault (`ENGINE_FAILED`), or `init()` was never called. Re-initialize, or stop. Everything else is `fatal: false` — the one operation failed and the SDK and the viewer are as they were: a page that did not render, a file that would not open (`CORS_ERROR`, `NETWORK_ERROR`, `FORMAT_ERROR`, `PASSWORD_ERROR`, `SECURITY_ERROR` — offer another file), an argument a call could not act on. `fatal` never says "the document is unusable"; the code does.

## Handling

```ts
import { Kernstra, SDKError, ErrorCode } from '@kernstra/pdf-web-sdk';

try {
  await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE });
} catch (err) {
  if (err instanceof SDKError) {
    // err.code is machine-readable; err.message is for logs, not users.
    if (err.code === ErrorCode.LICENSE_REQUIRED) {
      showLicenseHelp(Kernstra.getLicense());
    } else if (err.code === ErrorCode.ENGINE_LOAD_FAILED) {
      showSetupHelp();
    }
    if (err.fatal) {
      teardownAndReinitialize();
    }
  }
}

// Reported errors have no call to catch: they reach the viewer's error event.
viewer.on('error', ({ code, message, fatal }) => {
  if (fatal) {
    showReloadPrompt();
  } else if (code === ErrorCode.RENDER_FAILED) {
    // A page, a print job or the render worker; nothing to do but note it.
    telemetry.warn('pdf.render', message);
  }
});
```

## Codes

| Code | When | What to do |
| --- | --- | --- |
| `CORS_ERROR` | Loading by URL: the request was blocked before a response — usually the host does not allow cross-origin requests, but Chrome and Firefox report an offline or unreachable host the same way. With `credentials: 'include'` a cross-origin server must answer the page's exact origin and `Access-Control-Allow-Credentials: true`. | Allow your origin on the file host (or proxy the file through your own), check that the host is reachable, and load again. Not fatal: offer another file. |
| `NETWORK_ERROR` | Loading by URL: the server answered with an error status (e.g. 404; a `401` or `403` has its `message` end in the status) or the request failed in another way. `doc.whenFullyLoaded()`: a progressive download could not finish — a range request kept failing through its three retries, or the server reported a size the SDK cannot load (4 GiB or more) or a first range that was not the one asked for; the viewer reports it too. `uploadTo()`: the request could not be sent at all. | Check the URL and connectivity; for `401` / `403` refresh the token and call again. A progressive document stays open for viewing. Not fatal. |
| `FORMAT_ERROR` | Loading, `doc.pages.import()`, **Import PDF** in Manage Pages (reported): not a PDF, the file is corrupt, or it claims pages it does not hold (a page tree whose count the file cannot back). | Offer another file — `createLoadError()` shows it in place. Not fatal. |
| `PASSWORD_ERROR` | Loading, `doc.pages.import()`, **Import PDF** in Manage Pages (reported): a password is required, or the one supplied is wrong. Manage Pages asks for none. | Prompt for a password (`createPasswordPrompt()`) and load again with `{ password }`; import an encrypted file with `doc.pages.import(bytes, { position, password })`. |
| `SECURITY_ERROR` | Loading, `doc.pages.import()`, **Import PDF** in Manage Pages (reported): the file is encrypted with a security handler the SDK cannot open. | The document cannot be opened as supplied; offer another file. Not fatal. |
| `PAGE_ERROR` | `doc.pages`, `doc.annotations`, `doc.text`, bookmarks, attachments, `doc.security`, `viewer.tools.editText`, Manage Pages (reported), and `viewer.whenReady()` when the file's fields or pages cannot be wired: the SDK could not carry out a sound request on this document — a page that could not be loaded, copied, moved or created, a page tree nested too deeply for its pages to be changed (insert, delete, move, duplicate, import — rotate and extract still work), a page with form fields that could not be flattened for import, an annotation, bookmark, attachment or encryption that could not be written. | A state of the document, not of your code: report it and leave the document as it is. The viewer stands; the `error` event reports it too. Not fatal. |
| `RENDER_FAILED` | Rendering, printing, the render worker. Reported, not thrown — the page or print job failed, or the render worker did (rendering continues on the main thread). | Nothing to do but note it: the viewer stays usable. Not fatal. |
| `ENGINE_LOAD_FAILED` | `Kernstra.init()`: the asset store could not be reached on a first load, or served bytes that are not this SDK version's engine; or a self-hosted `engineUrl` is wrong. | **Fatal.** Fix the store's reachability (CSP `connect-src`, the network) or the self-hosted `engineUrl`, then call `Kernstra.init()` again — a failed call starts over on the next. |
| `ENGINE_FAILED` | Any document or viewer call, after the SDK stopped on an unrecoverable fault (a damaged or hostile file can cause one). Every later call answers this code. In the render worker it is reported, not fatal: the worker stops and rendering continues on the main thread. | **Fatal.** Nothing renders or saves until `Kernstra.destroy()` and `init()` again, then the document opened afresh. `destroy()` tears down whatever is open after this fault — that is the recovery. |
| `NOT_INITIALIZED` | Loading, `new PDFViewer`: a document was loaded, or a viewer constructed, before `Kernstra.init()` resolved. | Await `Kernstra.init()` at application startup before loading documents or constructing viewers. `fatal` is `true` when `init()` was never called. |
| `OUT_OF_MEMORY` | Any document call: the SDK could not allocate the memory one operation needed, usually for a very large page or document. | Not fatal — the next call works. Retry, or work on a smaller page or document. |
| `DOCUMENT_CLOSED` | Any document call: the document was closed or destroyed. The getters (`pageCount`, `metadata`, `pages.count`, …) throw it synchronously; every promise-returning member rejects with it. A viewer still open on a closed document refuses undo, redo, `snapshot()` and Download with a reported `DOCUMENT_CLOSED`, and its exporter rejects with it. | Check `doc.isClosed`; destroy the viewer or open a new document. A document closed after an `ENGINE_FAILED` fault is already released and its `close()` does nothing. |
| `EXPORT_FAILED` | Export, `doc.save()`, `doc.pages.extract()`, **Extract** in Manage Pages (reported): the document could not be written — including a changed PDF/A file whose claim cannot be removed. Thrown by the exporter's methods and `doc.save()`; reported (not thrown) when the toolbar's Download fails, and when an undo snapshot could not be recorded. | Read `message`, then retry. A change whose undo step could not be recorded still counts: `hasUnsavedChanges` turns true and `documentChanged` reports it. |
| `LICENSE_REQUIRED` | `Kernstra.init()`, `PDFDocument.fromUrl()` / `fromFile()` / `fromBytes()` / `fromUrlProgressive()`, `doc.pages.import()`, **Import PDF** in Manage Pages (reported): no license, or one that is expired, revoked, paused, for another origin, or unverifiable on a plain-HTTP page (reason `insecure-context`). The loaders refuse a stopped license only (not a signed-out one): no document opens under it. | Pass a valid token to `Kernstra.init({ license })` or `Kernstra.setLicense(token)` on a secure context (HTTPS or `localhost`); `Kernstra.getLicense()` says why the current one does not load. A paused license loads again once the licensing API says it is back in force. See [Licensing](https://kernstra.com/docs/web-sdk/guides/licensing). |
| `FEATURE_NOT_LICENSED` | Tools (`activateTool`, and the toolbar never shows them), export (`doc.save()`, `doc.pages.extract()` and **Extract** in Manage Pages, reported, too, and `doc.security.protect()` / `removeProtection()`), changing the document's structure (`edit-pages`: `doc.pages.insert()` / `delete()` / `move()` / `rotate()` / `setRotation()` / `duplicate()` / `import()`, `doc.outline.add()` / `delete()` / `set()` / `setTitle()`, `doc.attachments.add()` / `delete()`), print, forms, annotations (`forms` too for a form field's widget), reading content out (`extract`: `doc.text`, `doc.forms.fields`, `doc.forms.getValues`, `doc.attachments.data`, `viewer.search`, and opening or retyping a block through `viewer.tools.editText`); `init` for `engineUrl` / `shaperUrl` / `subsetterUrl`; the first text tool, or `new FontRegistry()`, for `fonts.fontsBaseUrl`. The license is valid but its `features` do not include what was asked, or a self-hosting URL was set without self-hosting in the license. | Check `Kernstra.getLicense()`: `state.valid && state.features.includes('export')`. Mirror `viewer.isToolEnabled(tool)` in your own chrome rather than offering a tool the license does not cover. See [Licensing](https://kernstra.com/docs/web-sdk/guides/licensing). |
| `INVALID_ARGUMENT` | Programmatic API: a call was given an argument it cannot act on — a page index out of range, an annotation id no annotation has, a bookmark path or attachment index that names nothing, an empty attachment name, deleting the only page, extracting or exporting no pages or a page the document lacks, a rotation that is not a multiple of 90, a position that is not a whole number, a text block id that names no block, recoloring a text box, an empty search query, `goToPage()` / `setScale()` out of range, `print({ pages })` with an empty list or a page the document has not, `activateTool()` for a name that is no tool or a tool this viewer's configuration turned off, `uploadTo()` `headers` holding a name or value a request cannot carry, a second `Kernstra.init()` with other options, a `styleNonce` that is not base64 or base64url, or a `licensePopup` part that is not a string. | A bug in the calling code, not a state of the document: fix the argument. |
| `INVALID_STATE` | A call that needs something first, or that a signed document rules out: a tool handle used while its tool is not active; a text block that kept losing its editing surface to another; on a progressive document still downloading, `doc.save()`, `doc.pages.extract()` and every write through `doc.annotations`, `doc.forms`, `doc.pages`, `doc.outline`, `doc.attachments` and `doc.security`; protecting, unprotecting or flattening a signed document, or a write a certified document's certification forbids, without `invalidateSignatures: true`; every acting member of a destroyed viewer; `Kernstra.destroy()` with a document open; an export from `createExporter()` whose pending Edit Text edits cannot be written, or whose document was reloaded under it. | Do the prerequisite first: `await viewer.activateTool('editText')` before using its handle, `await doc.whenFullyLoaded()` before writing (an export from `createExporter()` waits by itself), pass `invalidateSignatures: true` to go ahead on a signed document, destroy viewers and close documents before `Kernstra.destroy()`, export again after a reload. |
| `PERMISSION_DENIED` | `activateTool`, `print`: the document's permissions forbid it and it was not opened with its owner password. | There is no option to lift these: only the owner password does (`doc.security.unlockOwnerPermissions(password)`). `viewer.toolBlockedReason(tool)` gives the reason for a host's own chrome. |
| `ABORTED` | `PDFDocument.fromUrl()` / `fromUrlProgressive()`, `doc.whenFullyLoaded()`, export and `uploadTo()`, `viewer.whenReady()` when `destroy()` comes first: the `AbortSignal` you passed was aborted before the work was done. | Expected when you canceled; nothing to repair. |
| `UNKNOWN_ERROR` | Anywhere: unclassified. From `activateTool()`, a tool that could not start — nothing of it stays, the viewer goes back to the resting tool, and the cause is its `cause`. | Read `message` (and `cause` where set); log it, then retry or report. |
