It’s story time, join me in this video as I retell the story of a very subtle bug with a big impact.

In this video, Erik walks us through a real-world story about a subtle bug in AL code for Business Central — one that passed all testing but still caused problems in production. It’s a cautionary tale about how memory buffers work in loops, and why even diligent developers can miss issues that only surface under specific conditions.
The Task: Email Sales Invoices as PDFs
The requirement is straightforward: loop through unprinted posted sales invoices, render each one as a PDF, attach it to an email, and send it. Erik builds this out step by step, using well-known patterns and standard AL codeunits.
Here’s the code as written — and as tested — before the bug was discovered:
codeunit 50100 "Send Invoices as Emails"
{
procedure SendInvoicesAsEmails()
var
PSH: Record "Sales Invoice Header";
PSH2: Record "Sales Invoice Header";
OutS: OutStream;
InS: InStream;
TempBlob: Codeunit "Temp Blob";
Ref: RecordRef;
EmailMsg: Codeunit "Email Message";
Email: Codeunit Email;
Base64: Codeunit "Base64 Convert";
begin
PSH.Setrange("No. Printed", 0);
if PSH.FindSet() then
repeat
PSH2 := PSH;
PSH2.Setrange("No.", PSH."No.");
Ref.GetTable(PSH2);
TempBlob.CreateOutStream(OutS);
if Report.SaveAs(Report::"Sales Invoice NA", '', ReportFormat::Pdf, OutS, Ref) then begin
TempBlob.CreateInStream(InS);
EmailMsg.Create(PSH."Sell-to E-Mail", 'Your Invoice', 'Here you go!');
EmailMsg.AddAttachment('Invoice ' + PSH."No." + '.pdf', 'application/pdf', Base64.ToBase64(InS));
Email.Send(EmailMsg, "Email Scenario"::"Sales Invoice");
end;
until PSH.Next() = 0;
end;
}
Breaking Down the Pattern
Let’s walk through the key elements:
- Filtering: The code filters
Sales Invoice Headerto only unprinted invoices ("No. Printed" = 0). - Single-record filter for the report: A second variable
PSH2is created and filtered to exactly one invoice number. This ensures that no matter what the report’s internal logic does, only one invoice is passed in via theRecordRef. Erik notes this is a best practice — and there’s a separate video on this pattern. - Temp Blob as a memory buffer: A
Temp Blobcodeunit is used to hold the PDF output. AnOutStreamwrites the report into it, and anInStreamreads it back out for Base64 encoding. - Email creation and sending: The email module’s
Email Messagecodeunit creates the message, attaches the PDF (Base64-encoded), and sends it using an email scenario.
Everything Tested Fine — Until Production
This code was tested. It was tested with one invoice, with two, with a handful. Everything worked. PDFs opened fine. Emails arrived correctly.
Then it went to the customer. Phone calls started coming in: “We got the invoice, but we can’t open the attachment.”
The Subtle Bug: Temp Blob Is Not Cleared Between Iterations
Here’s the heart of the problem. The TempBlob variable is declared once and persists across all iterations of the loop. Calling CreateOutStream does not reset the blob’s internal buffer — it simply gives you a stream that starts writing at the beginning of whatever data is already there.
Consider this scenario:
- First iteration: The blob is empty. The report writes 100 KB of PDF data. You read out 100 KB. Everything is perfect.
- Second iteration: The blob still contains 100 KB from the first invoice. The report writes a smaller invoice — only 50 KB — overwriting the first 50 KB of the buffer. But the remaining 50 KB from the previous invoice is still there.
- You read out 100 KB — 50 KB of valid new data followed by 50 KB of leftover junk from the previous invoice.
The result is a corrupted PDF. Whether a PDF reader can handle the trailing junk depends on the reader and the specific data. Some readers gracefully ignore extra bytes at the end of a file; others refuse to open it. Some compressed internal structures within the PDF may or may not tolerate it. This makes the bug maddeningly intermittent — it only manifests when a smaller invoice follows a larger one, and even then, it might still open in some PDF readers.
The Fix: Clear the Temp Blob
The fix is a single line of code. Before creating the outstream in each loop iteration, clear the TempBlob:
if PSH.FindSet() then
repeat
PSH2 := PSH;
PSH2.Setrange("No.", PSH."No.");
Ref.GetTable(PSH2);
Clear(TempBlob); // <-- This is the fix
TempBlob.CreateOutStream(OutS);
if Report.SaveAs(Report::"Sales Invoice NA", '', ReportFormat::Pdf, OutS, Ref) then begin
TempBlob.CreateInStream(InS);
EmailMsg.Create(PSH."Sell-to E-Mail", 'Your Invoice', 'Here you go!');
EmailMsg.AddAttachment('Invoice ' + PSH."No." + '.pdf', 'application/pdf', Base64.ToBase64(InS));
Email.Send(EmailMsg, "Email Scenario"::"Sales Invoice");
end;
until PSH.Next() = 0;
Clear(TempBlob) resets the internal buffer, ensuring each iteration starts with a clean slate.
An Alternative: Extract to a Procedure
Erik also discusses a structural alternative. If you extract the body of the loop into its own procedure, the TempBlob variable becomes local to that procedure and is automatically initialized fresh on each call:
procedure SendInvoicesAsEmails()
var
PSH: Record "Sales Invoice Header";
begin
PSH.Setrange("No. Printed", 0);
if PSH.FindSet() then
repeat
PrintAndEmailSingleInvoice(PSH);
until PSH.Next() = 0;
end;
local procedure PrintAndEmailSingleInvoice(PSH: Record "Sales Invoice Header")
var
PSH2: Record "Sales Invoice Header";
OutS: OutStream;
InS: InStream;
TempBlob: Codeunit "Temp Blob";
Ref: RecordRef;
EmailMsg: Codeunit "Email Message";
Email: Codeunit Email;
Base64: Codeunit "Base64 Convert";
begin
PSH2 := PSH;
PSH2.Setrange("No.", PSH."No.");
Ref.GetTable(PSH2);
TempBlob.CreateOutStream(OutS);
if Report.SaveAs(Report::"Sales Invoice NA", '', ReportFormat::Pdf, OutS, Ref) then begin
TempBlob.CreateInStream(InS);
EmailMsg.Create(PSH."Sell-to E-Mail", 'Your Invoice', 'Here you go!');
EmailMsg.AddAttachment('Invoice ' + PSH."No." + '.pdf', 'application/pdf', Base64.ToBase64(InS));
Email.Send(EmailMsg, "Email Scenario"::"Sales Invoice");
end;
end;
With this approach, there's no loop variable persistence issue at all. Each call gets its own stack frame with a fresh TempBlob. Whether you prefer this refactoring or the explicit Clear(TempBlob) in the loop is a matter of style — both solve the problem.
Why This Bug Is So Hard to Catch
This bug has several properties that make it especially insidious:
- Works perfectly with a single record. You can never reproduce it if you only test with one invoice at a time.
- May work with multiple records. If invoices happen to be the same size or increasing in size, every PDF will be valid.
- Intermittent failures. It only corrupts when a smaller PDF follows a larger one in the sequence.
- Reader-dependent symptoms. Some PDF readers tolerate trailing junk; others don't. The same file might open in Adobe Reader but fail in a browser's built-in viewer.
- Testing at scale is impractical. If you send 100 test emails, do you actually open all 100 attachments? Maybe invoice #77 is the broken one.
Key Takeaways
This is a great example of a bug that survives careful development and testing. The lessons here apply broadly:
- Understand your buffer lifecycle.
CreateOutStreamdoes not clear the underlyingTemp Blob. It simply provides a stream to write into it. AlwaysClear(TempBlob)when reusing it in a loop. - Be wary of variables that survive loop iterations. Any variable declared outside a
repeat...untilblock retains its state. This is by design, but it can bite you when you assume a clean state. - Consider extracting loop bodies into procedures. This naturally scopes variables to each iteration and eliminates an entire class of stale-state bugs.
- Test with varying data sizes. When working with file generation in loops, test with documents of different sizes — specifically, test with a large document followed by a small one.
Sometimes you do everything right — you follow best practices, you test thoroughly, and you still get bitten by a one-liner. That's the nature of subtle bugs, and it's exactly why understanding the underlying mechanics of the tools you use matters so much.