Why your invoice PDF comes out blank and nothing says why
“The invoice PDF generates with a blank field, or ignores a choice I made, and nothing reports an error.”
You generate an invoice PDF and something is wrong with it. The invoice date is missing. Or the page is entirely white. Or the customer received the attachment you unticked. There is nothing in the log and nothing on screen, and the next invoice looks fine.
We read a little over twenty recent public bug reports about invoice PDFs. Many of them were requests for a PDF that did not exist yet, or about layout. Among the ones where the PDF came out wrong and nothing said so, about half came down to the same mistake, and it is worth knowing by name because it is easy to find once you look for it.
"No value" has more than one spelling
Data that means "nothing here" can reach your PDF code in several shapes:
- the key is missing from the object or array altogether
- the key is there and holds
null(orundefinedin JavaScript) - the key holds an empty string
- a form field was never submitted, so there is no key at all
- a database column stays
nulluntil some later event fills it
And two shapes that look empty and are not: 0 and false. Zero hours is a value.
Code that handles "no value" usually handles one of these, typically whichever one the test data happened to use. Production data uses the others. Nothing fails at the point of the mistake. It fails later, inside the renderer, where the failure is easiest to lose.
Guarding the key and missing the value
One report described a line item template that wrote the hours cell like this:
l.hours !== undefined ? formatHours(l.hours) : ""
That checks whether the key is absent. A line that carries hours: null passes the
check, reaches formatHours(null), which calls .toFixed on nothing, and throws.
The page never finishes building and renders as a blank white sheet. The actual error
is in the browser console, where nobody generating an invoice is looking.
The line that triggered it was ordinary: an extra charge such as a rush fee, which has no hours. The project's own test data happened to omit the key for those lines, so the other spelling was never exercised.
The guard that covers both spellings in JavaScript is == null, which is true for
null and undefined and nothing else. Here is how the common guards behave, checked in
Node:
hours is |
!== undefined |
== null |
truthiness (if (hours)) |
|---|---|---|---|
| key missing | treated as empty | treated as empty | treated as empty |
null |
throws | treated as empty | treated as empty |
0 |
formatted | formatted | treated as empty |
"" |
throws | throws | treated as empty |
No single guard is right for every row. The table is the argument for deciding what an empty value looks like before the data reaches the template, rather than inside it.
An unticked checkbox is not "no"
Another report: an email composer offered a checkbox to attach the invoice PDF. The user unticked it, and the email went out with the PDF attached.
This is how HTML forms work, and MDN states it plainly:
If a checkbox is unchecked when its form is submitted, neither the name nor the value is submitted to the server.
So the server received nothing for that field, which is exactly what it receives when the form was never shown at all. It could not tell "the user said no" from "the user said nothing", took the second reading, and fell back to the template's default, which was to attach.
The fix proposed in the report is the general one: submit a separate marker with the form, and use the default only when the marker is missing. If the marker is present and the checkbox is not, the answer is no.
Turning empty into missing
A third project cleaned its data before rendering. Company details went through a loop that removed every empty value, so an unset address line was deleted from the array rather than kept as empty.
The PDF template then asked for the address line, and the template engine was running with strict variables. Twig's documentation describes that setting:
If set to
false, Twig will silently ignore invalid variables (variables and or attributes/methods that do not exist) and replace them with anullvalue. When set totrue, Twig throws an exception instead
So a company that had simply not filled in its second address line could not produce an
invoice. The fix kept every key present and set the empty ones to null, and the
template used a default for each.
The cleanup was well meant. It converted one spelling of nothing into another, and the code downstream only accepted the first.
Note that turning strict mode off would also have made the error go away, by printing
nothing where the value should be, which is the silent version of the same bug. Twig's
own default filter carries a warning in this territory too: it treats false as
empty, so {{ value|default(true) }} prints true when the value is false. Its
documentation suggests ?? for that case.
A field that is empty until later
A fourth: an order page offered an "Invoice" button on every order, including unpaid
ones. The invoice date is only written when the order is marked paid, so on an unpaid
order it is null and the PDF printed "Invoice date :" with nothing after it.
Here the null is correct. The order genuinely has no invoice date yet. The bug is that
the document was offered before the data it depends on could exist. The report suggests
the two reasonable fixes: hide the button until the order is paid, or fall back to a
date that does exist and make that visible.
The mirror image turned up too, and it is loud rather than silent. A project added a new required field for an e-invoicing standard, and invoices issued before the field existed could no longer produce a PDF at all, because validation treated "this did not exist when the invoice was issued" the same as "this is missing".
The catch that hides all of the above
Finally, one report was about none of these causes and all of them. The invoice toolbar caught every PDF failure, logged it only in development, and put the download button back to its idle label. Any of the problems above, in a codebase built like that, would look like the button doing nothing.
That is what turns a bug into a silent one. A missing hours value that throws is a good failure, if the throw reaches a person.
Fixing it, in order
- Decide what "nothing" looks like at one boundary. Where data enters the PDF code,
convert every spelling of empty into one, usually
null, and keep every key present. Do this once, in one place, and nowhere else. - Guard the value, not the key. In JavaScript that is
== nullrather than!== undefined. Do not use truthiness on numbers or booleans, because0andfalseare values. - Never infer "no" from a missing form field. Send a marker so the server knows the form was submitted, then read a missing checkbox as unticked.
- Keep strict mode on in templates and make the data complete, rather than switching it off to make the error go away.
- Ask whether the document should exist yet. A required field that is legitimately empty is a reason not to offer the PDF, or to say what was substituted.
- Show render failures where the button is. Not in a development log, and not as a button that quietly resets.
How to check your own
None of this needs a tool.
- Give your test data one line per spelling: key omitted,
null, empty string,0. One of the projects above fixed its bug this way, by making its own fixture carry both spellings so the case could not go untested again. - Render every fixture, not only the one the page opens on, and read the console or the server log after each.
- Submit the form once with each box ticked and once with it unticked, and open the attachment that actually arrives.
- Generate an invoice for an order in every state you have: unpaid, paid, refunded, and one created before your most recent schema change.
- Search the PDF code for
!== undefinedand for bareif (value)on numeric fields.
What this post covers. It is drawn from public issue reports filed on GitHub in August and September 2026, mostly on small and mid size projects. The projects are not named and the code sample is a simplified illustration written for this post. The JavaScript behaviour in the table was run in Node. The checkbox behaviour is quoted from MDN and the template behaviour from Twig's documentation. It does not cover PDF libraries, fonts, layout, or the content rules of any e-invoicing standard.