100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Debugging Pascal Programs

Learn practical techniques for diagnosing runtime errors, memory issues, and logic bugs in Free Pascal programs using the Lazarus debugger and compiler checks.

Practical PascalIntermediate9 min readJul 10, 2026
Analogies

Compiler Checks and Runtime Errors

Free Pascal's compiler directives let you trade performance for safety during development: {$R+} enables range checking on array/string indexing, {$Q+} enables overflow checking on integer arithmetic, and {$C+} (or {$ASSERTIONS ON}) enables Assert statements to actually execute rather than compile to no-ops. When one of these checks fails, the program halts with a specific numbered runtime error — 201 for range-check errors, 216 for a general protection fault often caused by dereferencing a nil or dangling pointer, and 106 for an invalid numeric format during type conversion — and the exact error number is the fastest way to narrow down what category of bug you're dealing with before even opening the debugger. Production release builds typically disable these checks with {$R-}{$Q-} for performance, so it's standard practice to keep a debug build configuration in Lazarus with all checks enabled.

🏏

Cricket analogy: Range checking ({$R+}) is like a third umpire automatically flagging when a fielder's throw goes beyond the boundary rope before anyone even reviews the footage — an early, specific signal (runtime error 201) rather than letting an invalid play stand unnoticed.

Using the Lazarus Debugger

Lazarus's integrated debugger (typically backed by GDB, or LLDB on macOS) lets you set breakpoints by clicking the gutter next to a line, then step through code with F7 (Step Into), F8 (Step Over), and Shift+F8 (Step Out), inspecting live variable values in the Watches and Local Variables windows as execution pauses. Conditional breakpoints — right-click a breakpoint and set a condition like i = 47 — are essential when a bug only manifests deep inside a loop, since stepping manually through thousands of iterations to reach iteration 47 would be impractical; you can also set a 'pass count' so a breakpoint only triggers on, say, the 10th time it is hit. For a segmentation fault (runtime error 216) with no useful call stack, enabling {$C+} and stack-trace-friendly compiler options (-gl for line info, -gw for DWARF debug info) ensures the debugger's Call Stack window shows meaningful unit/line information instead of raw memory addresses.

🏏

Cricket analogy: Setting a conditional breakpoint like i=47 is like a coach only reviewing footage from the exact over a specific bowling error occurred, rather than rewatching an entire day's play — targeted inspection instead of exhaustively stepping through every ball.

Common Bug Patterns and Assertions

A frequent Pascal bug pattern is accessing a class field before calling the constructor, or calling a method on a nil object reference after a failed Create or an earlier Free — enabling {$C+} and sprinkling Assert(Obj <> nil, 'Obj was not created') at suspicious call sites turns a silent nil-dereference crash into an immediate, informative failure at the exact point of misuse rather than several stack frames later. Off-by-one errors in loops are another classic, especially around High()/Low() versus hardcoded bounds, since Pascal arrays are not guaranteed to be zero-based — a for i := 0 to Length(Arr) - 1 loop over an array declared with a nonstandard lower bound can silently skip or overrun elements, which is exactly the class of bug that {$R+} range checking is designed to catch during development.

🏏

Cricket analogy: An Assert catching a nil object is like a fielding coach checking a player has actually put on protective gear before entering a drill, immediately flagging the omission rather than discovering it only after an injury occurs mid-session.

pascal
{$mode objfpc}{$H+}
{$C+}      // enable assertions
{$R+}      // enable range checking

program DebugDemo;

uses
  SysUtils;

type
  TWidget = class
    Value: Integer;
  end;

var
  W: TWidget;
  Arr: array of Integer;
  i, Sum: Integer;
begin
  W := nil;
  Assert(W <> nil, 'W was not created before use'); // fails loudly, with message

  SetLength(Arr, 5);
  for i := 0 to 3 do
    Arr[i] := i * 2;

  Sum := 0;
  for i := Low(Arr) to High(Arr) do  // safer than hardcoded 0..4
    Sum := Sum + Arr[i];             // Arr[4] is uninitialized -> caught if used meaningfully

  WriteLn('Sum = ', Sum);
end.

Free Pascal's SysUtils unit provides GetExceptionStack and Exception.StackTrace helpers, but for reliable stack traces you must compile with -gl (line info) and often link a symbol-lookup unit like LineInfo or heaptrc into the uses clause; heaptrc additionally reports memory leaks at program exit, listing exactly which GetMem/New call was never matched by a FreeMem/Dispose.

  • Enable {$R+}, {$Q+}, and {$C+} in debug builds to catch range, overflow, and assertion failures early.
  • Numbered runtime errors (201 range check, 216 access violation, 106 invalid format) narrow down the bug category immediately.
  • The Lazarus debugger (GDB/LLDB-backed) supports breakpoints, conditional breakpoints, and pass counts.
  • F7/F8/Shift+F8 step Into/Over/Out of function calls while inspecting the Watches and Local Variables windows.
  • Compile with -gl/-gw for meaningful call stacks instead of raw memory addresses on crashes.
  • Assert statements catch nil-reference and precondition bugs immediately rather than several stack frames later.
  • The heaptrc unit reports unmatched GetMem/New allocations as memory leaks at program exit.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#PascalStudyNotes#DebuggingPascalPrograms#Debugging#Pascal#Programs#Compiler#StudyNotes#SkillVeris#ExamPrep