Skip to main content

What can and cannot fail a generation

generatePdf draws one line: a problem in the data, or in malformed content, degrades — the field renders empty, a character is replaced, the PDF comes out. A structural problem, or one where content would silently disappear from a document someone signs, fails loudly. A 200-page report must not die because one row had a \n in it.

Degrades (the PDF comes out):

SituationWhat happens
Control character in the data (\n, \t, NUL, C0/C1)Replaced with a space. No font has a glyph for these, so it is the only possible rendering
Invalid {...} expressionThat token renders empty; the editor flags the field
Path that does not resolveEmpty
data that is null, an array, a string, a numberIgnored; fields render empty
Bound array that is a number, items that are not objects, a cell whose value is an objectIgnored or stringified
Invalid colour ("banana", "#zzz")Falls back to the default
fontSize that is 0, negative or NaN0/negative are honoured; NaN falls back to 10
A field width of 0 or negativeHonoured (nothing is drawn)
Image binding resolving to something that is not a data URIField stays empty
A section repeat taller than a whole pagePlaced anyway, overflowing that page; the next repeat starts on a new one

Fails loudly (and why):

Every one of these is a typed class with a literal code, exported from both json-pdf-designer and json-pdf-designer/server. The column below is what you match on — never error.message, which is an English diagnostic string and may be reworded in a patch release.

SituationClass (code)blameWhy not degrade
Character with no glyph in the standard font (emoji/CJK without fontBytes)UnsupportedGlyphError (unsupportedGlyph)dataIt does have a glyph in a complete font — dropping it would silently remove content from a signed document. Pass fontBytes, or take it out of the data
Page size missing, not an object, or not two finite numbers > 0InvalidPageSizeError (invalidPageSize)templateStructural: there is no sensible default to guess. Checked for every page before anything is rendered, so a bad page 7 does not wait for six good ones
Corrupt image in a fieldImageUnreadableError (imageUnreadable)templateThe author picked that file; silently dropping it hides the mistake
Corrupt image as the page backgroundBackgroundImageUnreadableError (backgroundImageUnreadable)templateSame, and a missing letterhead is not obvious from the output
Image format that is not PNG or JPEGUnsupportedImageFormatError (unsupportedImageFormat)templateRe-upload through the editor, which converts
A single image over 15MB (decoded)ImageTooLargeError (imageTooLarge)templateProtects whoever is generating — a template can come from an untrusted source
More than 200 distinct images in one documentTooManyImagesError (tooManyImages)templateSame. Identical images are deduped by content first, so this counts real ones
An upload over 20MB, or one the browser cannot readImageUploadTooLargeError / ImageUploadUnreadableErrordataCaught at upload, before it can reach a template
Document over maxPages (default DEFAULT_MAX_PAGES, 5000)PageLimitError (pageLimit)dataA truncated report that looks complete is worse than no report. Filter the data, split into several PDFs, or raise maxPages
A pagination pass that consumes nothingPaginationStalledError (paginationStalled)packageArithmetic bug in the package — spinning until a counter ran out used to hide it
.woff2 passed where the runtime cannot decompress itWoff2SupportMissingError / FontDecompressFailedError / FontDecompressTimeoutErrorconfigAn environment problem, not a template one
Template that is not an object, or a version that is not a numberTemplateNotAnObjectError / TemplateVersionInvalidErrortemplateThere is nothing to migrate from
Template.version newer than this buildTemplateVersionTooNewError (templateVersionTooNew)templateA newer format may carry fields this build would ignore in silence
A migration step missing between two versionsTemplateMigrationMissingError (templateMigrationMissing)packageOur gap, not yours
Invalid fontBytes (Unknown font format)not ours — pdf-lib throws it—Caller error, not template or data. See the null case below

Handling one​

Three lines, and the order matters:

import { describePdfError, dictFor, PageLimitError } from "json-pdf-designer/server";

try {
const bytes = await generatePdf(template, data, bindings, { fontBytes });
} catch (err) {
// 1. A specific case you want to treat specially — by class, not by message.
if (err instanceof PageLimitError) return splitAndRetry(err.maxPages);

// 2. Everything else: one call turns it into copy a person can read.
const problem = describePdfError(err, dictFor("en"));
// → { code, blame, title, action?, field?, detail } | null
}

describePdfError is exhaustive, so there is no "I did not recognise this one" branch inside it. It returns null for an error that is not the package's — the Unknown font format row above is exactly that case — instead of inventing a title for a failure it does not understand. Keep your own copy for your own errors, and fall through to a generic only when both come up empty.

If you want the narrowing without the copy — logging, metrics, a status code and nothing shown to anyone — isPdfError(err) narrows unknown to AnyPdfError, which gives you code and blame with no dictionary involved.

Never put the described text in state

title and action come out in the language of the dict you passed. Store them, and they freeze in that language — so a user switching the UI language sees that one part of the screen left behind, with nothing failing and nothing logged. Store the raw error, describe it while rendering.

- catch (err) { setError(describePdfError(err, dictFor(locale))); }
+ catch (err) { setErrorBox({ err }); }
+ // …and in the render body, so it re-derives on every locale change:
+ const problem = errorBox && describePdfError(errorBox.err, dictFor(locale));

The box ({ err }) is not decoration: null cannot tell "no error" from "an error that is null", and setError(err) with an error that happens to be a function would be read by React as an updater.

On a server​

blame exists so this decision does not have to be made twice:

blameMeaningReasonable status
dataThe payload cannot produce this document400, or 413 for pageLimit
templateThe saved template is wrong422
configThe environment is missing something (a font, a codec)500, and page someone
packageOur bug500, and please open an issue

Nothing above needs React: the classes, isPdfError, describePdfError and dictFor all come from json-pdf-designer/server.

One caveat on the glyph error. It only fires with the standard font (Helvetica/WinAnsi). With a font embedded through fontBytes, a character the font does not cover is drawn as that font's .notdef glyph — usually a blank or a box — with no error at all. That is fontkit's behaviour, not a choice this package makes, and it means a custom font trades a loud failure for a silent blank. If a missing glyph must never pass unnoticed, check the data before generating.