Delphi Procedures, Functions and Units Explained Simply

When you start with Delphi, it’s tempting to put all your code inside button click events. That works for ten lines. At a hundred lines it turns into a mess: the same calculation is copied into three buttons, and fixing a bug means fixing it three times.

The cure is to split your code into procedures and functions, and to group them into units. This article shows you how, with small examples you can run in a console app.

It’s part 3 of the Learn Delphi series. If you’re new, start with What Is Delphi? and Delphi basics.

Diagram: a form calling routines from a unit with public interface and private implementation, plus parameter kinds

Procedure vs Function

Both are named blocks of code you can call from anywhere. The difference is simple:

ProcedureFunction
Returns a value?NoYes
Use it forDoing something: show, save, print, updateCalculating something: a total, a check, a formatted text
Called likePrintInvoice(42);Total := CalcTotal(Price, Qty);

A Procedure

procedure PrintLine(const Text: string);
begin
  WriteLn('| ', Text);
end;

Call it with PrintLine('Hello');

A Function

function AddVat(Amount: Currency): Currency;
begin
  Result := Amount * 1.10;
end;

Call it with PriceWithVat := AddVat(100);

Inside a function, Result is a special variable that holds the return value. You can set it, read it and change it as many times as you like. Whatever it holds when the function ends is returned.

You may also see old code that assigns to the function name (AddVat := Amount * 1.10;). It works, but Result is clearer and is the modern style.

Use Exit to leave early. Since Delphi 2009 you can also pass the result to it:

function SafeDivide(A, B: Double): Double;
begin
  if B = 0 then
    Exit(0);        // same as: Result := 0; Exit;
  Result := A / B;
end;

Where Do They Go?

In a console program, you declare procedures and functions before the main begin ... end. block:

program Shop;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

function AddVat(Amount: Currency): Currency;
begin
  Result := Amount * 1.10;
end;

procedure PrintPrice(const Name: string; Price: Currency);
begin
  WriteLn(Format('%-10s %8.2f', [Name, AddVat(Price)]));
end;

begin
  PrintPrice('Keyboard', 25);
  PrintPrice('Monitor', 180);
  ReadLn;
end.

Output:

Keyboard      27.50
Monitor      198.00

The rule: a routine must be declared before it’s used. PrintPrice can call AddVat because AddVat is above it.

Local Variables

A routine can have its own var section. Those variables exist only while the routine runs:

function SumTo(N: Integer): Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := 1 to N do
    Result := Result + I;
end;

Prefer local variables over global ones. Global variables can be changed from anywhere, which makes bugs hard to find.

Parameters: Value, const, var and out

This is the part that confuses beginners most, and it matters. Delphi has four ways to pass a parameter:

KindSyntaxCan the routine change it?Does the caller see the change?Use it for
ValueX: IntegerYes, its own copyNoSmall inputs
constconst S: stringNoNot applicableInputs you won’t change, especially strings, records and arrays
varvar Total: CurrencyYesYesUpdating a value the caller owns
outout Msg: stringYesYesReturning extra values; the input value is ignored

See them side by side:

procedure ByValue(X: Integer);
begin
  X := X * 2;            // changes only the local copy
end;

procedure ByVar(var X: Integer);
begin
  X := X * 2;            // changes the caller's variable
end;

var
  N: Integer;
begin
  N := 5;
  ByValue(N);
  WriteLn(N);            // 5
  ByVar(N);
  WriteLn(N);            // 10
end.

When to Use const

Mark string, record and array parameters as const when the routine only reads them:

function Initials(const FullName: string): string;

It tells the reader “this input won’t change”, the compiler stops you from changing it by accident, and it avoids some extra work when passing strings and records.

Returning Several Values with out

A function returns one value. When you need more, use out parameters, often with a Boolean result that says whether it worked:

function TryParseQty(const Text: string; out Qty: Integer;
  out ErrorMsg: string): Boolean;
begin
  ErrorMsg := '';
  Result := TryStrToInt(Text, Qty);
  if not Result then
    ErrorMsg := '"' + Text + '" is not a number'
  else if Qty <= 0 then
  begin
    ErrorMsg := 'Quantity must be greater than zero';
    Result := False;
  end;
end;
if TryParseQty(edtQty.Text, Qty, Msg) then
  AddToCart(Qty)
else
  ShowMessage(Msg);

This Try... pattern is used throughout Delphi itself: TryStrToInt, TryStrToDate, TryStrToFloat.

Default Parameters

Give a parameter a default value, and the caller can leave it out. Default parameters must come last:

function FormatMoney(Amount: Currency; const Symbol: string = 'BHD';
  Decimals: Integer = 3): string;
begin
  Result := Format('%s %.*f', [Symbol, Decimals, Amount]);
end;

begin
  WriteLn(FormatMoney(12.5));               // BHD 12.500
  WriteLn(FormatMoney(12.5, 'USD', 2));     // USD 12.50
end.

Overloading: Same Name, Different Parameters

Mark routines with overload to give them the same name but different parameter lists:

function Area(Side: Double): Double; overload;
begin
  Result := Side * Side;
end;

function Area(Width, Height: Double): Double; overload;
begin
  Result := Width * Height;
end;

begin
  WriteLn(Area(3):0:1);       // 9.0
  WriteLn(Area(3, 4):0:1);    // 12.0
end.

Delphi picks the version that matches the arguments. (:0:1 in WriteLn formats a number with 1 decimal place.)

Units: Organising Code in Files

A unit is a .pas file that groups related routines, types and constants. You already have one for each form. You can create your own: File > New > Unit – Delphi.

Every unit has the same shape:

unit PriceUtils;

interface

// Public: other units can use what's declared here
const
  VatRate = 0.10;

function AddVat(Amount: Currency): Currency;
function FormatMoney(Amount: Currency; const Symbol: string = 'BHD'): string;

implementation

uses
  System.SysUtils;   // only needed inside this unit

// Private helper: not listed in interface, so other units can't see it
function RoundTo3(Value: Currency): Currency;
begin
  Result := Round(Value * 1000) / 1000;
end;

function AddVat(Amount: Currency): Currency;
begin
  Result := RoundTo3(Amount * (1 + VatRate));
end;

function FormatMoney(Amount: Currency; const Symbol: string): string;
begin
  Result := Format('%s %.3f', [Symbol, Amount]);
end;

end.

Notice:

  • interface is the public part. Only the declaration (the first line) of each routine goes here.
  • implementation holds the full code, plus any private helpers like RoundTo3.
  • Default values only need to be written in the interface declaration.
  • A unit ends with end. (with a full stop).

To use the unit from a form, add it to the uses clause:

implementation

uses
  PriceUtils;

procedure TForm1.btnCalcClick(Sender: TObject);
begin
  lblTotal.Caption := FormatMoney(AddVat(StrToCurr(edtPrice.Text)));
end;

Interface uses or Implementation uses?

  • Put a unit in the interface uses only if the interface part needs it (for example, a type used in a public declaration).
  • Otherwise put it in the implementation uses.

This keeps units loosely connected and avoids the “circular unit reference” error you get when two units list each other in their interface sections.

Good Units to Create

UnitWhat goes in it
PriceUtils.pasMoney, VAT and discount calculations
DateUtilsEx.pasYour own date helpers (month-end, fiscal year)
Validation.pasTryParse... and IsValid... checks
DataModule.pasThe database connection and shared queries

A rule of thumb: forms handle the screen, units handle the logic. A click handler should be a few lines that call routines from your units. Then you can reuse and test the logic without the form.

Nested Routines

Delphi lets you declare a small routine inside another one. The inner routine can see the outer one’s parameters and local variables:

procedure PrintReport(const Title: string);

  procedure Line;
  begin
    WriteLn(StringOfChar('-', Length(Title)));
  end;

begin
  Line;
  WriteLn(Title);
  Line;
end;

Use this for small helpers that only make sense inside one routine.

Common Beginner Mistakes

  1. Calling a routine before it’s declared. Move it higher, or declare it in the unit’s interface.
  2. Forgetting var when the caller should see the change. Without var, the routine only changes its own copy.
  3. Not setting Result on every path. If one if branch forgets, the function returns garbage. The compiler warns: “Return value of function might be undefined”. Read the warnings.
  4. Copy-pasting logic into several click events. Move it into a function in a unit and call it.
  5. Putting everything in the interface uses. Use the implementation uses unless the interface needs it.

Practice Exercises

Exercise 1. Write a function IsLeapYear(Year: Integer): Boolean. A year is a leap year if it’s divisible by 4, except years divisible by 100, unless they’re also divisible by 400.

Exercise 2. Write a procedure Swap(var A, B: Integer) that swaps two values.

Exercise 3. Write a function Initials(const FullName: string): string that returns 'SA' for 'Sara Ali'.

Answers

Exercise 1:

function IsLeapYear(Year: Integer): Boolean;
begin
  Result := ((Year mod 4 = 0) and (Year mod 100 <> 0)) or (Year mod 400 = 0);
end;
// IsLeapYear(2024) = True, IsLeapYear(1900) = False, IsLeapYear(2000) = True

(Delphi also has a built-in IsLeapYear in System.SysUtils. Writing your own is good practice.)

Exercise 2:

procedure Swap(var A, B: Integer);
var
  Temp: Integer;
begin
  Temp := A;
  A := B;
  B := Temp;
end;

Exercise 3:

function Initials(const FullName: string): string;
var
  I: Integer;
  S: string;
begin
  Result := '';
  S := Trim(FullName);
  for I := 1 to Length(S) do
    if (I = 1) or (S[I - 1] = ' ') then
      if S[I] <> ' ' then
        Result := Result + UpperCase(S[I]);
end;

Summary

  • A procedure does something; a function returns a value through Result.
  • Parameters are passed by value by default. Use const for inputs you won’t change, var to update the caller’s variable, and out for extra return values.
  • Default parameters go last, and overload lets routines share a name.
  • A unit has a public interface and a private implementation. Keep logic in units and keep forms thin.

Next in the series: classes and objects, Delphi’s way of grouping data and the code that works on it. All parts are in the Delphi category.

Related articles

Scroll to Top