Sometimes, it’s those subtle bugs that will get you!

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

https://youtu.be/i17d1BLUPfM

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 Header to only unprinted invoices ("No. Printed" = 0).
  • Single-record filter for the report: A second variable PSH2 is 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 the RecordRef. Erik notes this is a best practice — and there’s a separate video on this pattern.
  • Temp Blob as a memory buffer: A Temp Blob codeunit is used to hold the PDF output. An OutStream writes the report into it, and an InStream reads it back out for Base64 encoding.
  • Email creation and sending: The email module’s Email Message codeunit 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:

  1. First iteration: The blob is empty. The report writes 100 KB of PDF data. You read out 100 KB. Everything is perfect.
  2. 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.
  3. 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. CreateOutStream does not clear the underlying Temp Blob. It simply provides a stream to write into it. Always Clear(TempBlob) when reusing it in a loop.
  • Be wary of variables that survive loop iterations. Any variable declared outside a repeat...until block 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.