How to Manage Pascal Resources with Automatic Cleanup

A routine that creates a list, a stream and a temporary buffer needs more than a happy-path Free. An exception can occur after any successful allocation. Temporary flags need restoring too, and the callback that restores them must not outlive the object it calls.

Sphere 10 Pascal Framework provides two complementary tools. TDisposables records resources owned by a scope and disposes them in reverse registration order. TScope retains one deferred cleanup method through a managed record. Together they let us express both resource ownership and temporary state restoration without hiding where ownership eventually moves.

Resources retained within a deliberate cleanup boundary.
Resources retained within a deliberate cleanup boundary.

Contents

Choose the Owner Before the Syntax

A resource bag is useful when one routine or object genuinely owns several resources. Register each successfully created object immediately. Do not register an object borrowed from a caller merely because it is convenient to have a bag nearby. Ownership remains a contract, not a side effect of using a helper.

A deferred cleanup scope is appropriate for a paired action: restore a flag, release a lock or undo temporary presentation state. Its callback target is borrowed and must remain alive until cleanup runs. In the example, the outer TState instance outlives MakeReport, so its Restore method remains valid during exception unwinding.

Exercise an Exception Path

The report routine creates two owned objects, installs the state-restoration callback and then deliberately raises. Its caller catches that example exception and verifies that Busy is false. The program also demonstrates an explicit ownership transfer when CreateOwnedLines removes the returned list from its local bag, leaving the caller responsible for freeing it.

This second path matters. Returning a reference to an object still owned by a local disposal bag would return an object whose lifetime has already ended. Removing it from the bag is the visible handover, and the caller's try/finally completes the new contract.


program ArticleSample;
{$IFDEF FPC}{$mode Delphi}{$H+}{$ENDIF}
uses Classes, SysUtils, UCommon, UMemory;
type
  TState = class
  public
    Busy: Boolean;
    procedure Restore;
  end;

procedure TState.Restore;
begin
  Busy := False;
end;

procedure MakeReport(State: TState);
var
  Resources: TDisposables;
  Lines: TStringList;
  Text: TStringStream;
  Cleanup: TScope;
begin
  Lines := Resources.AddObject(TStringList.Create) as TStringList;
  Text := Resources.AddObject(TStringStream.Create('')) as TStringStream;
  Cleanup := TScope.Create(State.Restore);
  State.Busy := True;
  Lines.Add('Report ready');
  Text.WriteString(Lines.Text);
  WriteLn(Trim(Text.DataString));
  raise Exception.Create('Demonstrate stack unwinding');
end;

function CreateOwnedLines: TStringList;
var Resources: TDisposables;
begin
  Result := Resources.AddObject(TStringList.Create) as TStringList;
  Result.Add('Caller owns this list');
  Resources.RemoveObject(Result);
end;

var State: TState; Lines: TStringList;
begin
  State := TState.Create;
  try
    try
      MakeReport(State);
    except
      on E: Exception do WriteLn(E.Message);
    end;
    if State.Busy then raise Exception.Create('Cleanup did not restore state');
    Lines := CreateOwnedLines;
    try
      WriteLn(Lines[0]);
    finally
      Lines.Free;
    end;
    WriteLn('Busy=False after exception');
  finally
    State.Free;
  end;
end.

Observe Cleanup and Transfer


Report ready
Demonstrate stack unwinding
Caller owns this list
Busy=False after exception

The report text is produced before the deliberate exception. After unwinding, the deferred cleanup has restored the flag. The returned list is still usable because ownership was transferred, and the caller frees it normally. These are behavior checks, not a claim that every possible disposal callback has been tested by this small example.

Register dependencies in a sensible order. If one owned object borrows another, register the borrowed dependency first and its consumer afterwards. Reverse disposal then retires the consumer before the dependency. This simple rule keeps streams, writers and their backing objects in a readable relationship.

Understand Managed Scope Lifetime

TScope copies share one cleanup guardian. Cleanup occurs when the last managed owner releases it, not once for every record copy. Retaining a scope in a field or returning it therefore extends the acquisition's lifetime. This is useful when intentional and surprising when treated like an ordinary unowned value.

A nested begin/end is not a portable way to force an earlier local managed-record finalization. Put a shorter lifetime in a separate routine, as this example does. For thread-affine resources, all retained copies must end on the acquiring thread, and the resource itself must outlive those scopes.

⚠️ Cleanup callbacks should have a clear, appropriate failure policy. Automatic lifetime management does not make a callback incapable of raising, nor does it authorize suppressing an important cleanup failure.

Use the Tools Where They Clarify Code

Keep a straightforward try/finally when one resource already has an obvious short owner. A disposal bag becomes valuable when it removes repetitive nested cleanup while preserving the same visible ownership contract. A scope becomes valuable when a paired state change should survive every routine exit.

The scope type , disposal bag and worked examples show these patterns in the actual Framework. Use them for the mechanics of ownership, while leaving commit, rollback, retry and other business decisions explicit in the application that owns them.

Get the Framework and Run the Example

Get Sphere 10 Pascal Framework and follow its package instructions . Use the units directly, or build the optional Lazarus .lpk or Delphi runtime package. Pascal uses these source/package workflows. The C# Framework is distributed separately through NuGet.

The attached application sample was compiled and executed with Free Pascal 3.2.0 on Windows against this public source revision , with range and overflow checks. Its Build.ps1 accepts the Framework, compiler, Lazarus and output paths so the example does not need machine-specific paths. Lazarus UI examples require the LCL Win32 widgetset. A successful FPC run does not claim a new Delphi build. Follow the repository's separate Delphi/VCL project and resource instructions there.

Download the complete runnable sample.

sample.zip
Download the complete worked example and build instructions (ZIP).

Continue the Series

How to Upgrade a Read Lock Safely in Delphi and Free Pascal

How to Design a Cache Around the Value’s Lifecycle

How to Buffer a Pascal Stream Without Breaking Its Cursor

For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .