PL/pgSQL Block Labels
Master PL/pgSQL block labels — the <<label>> syntax placed immediately before BEGIN (NOT before $$), used for disambiguating shadowed variable names via outer.variable, EXIT label and CONTINUE label for multi-level loop control, and labeled exception scopes. Worked examples include shadowed-variable resolution, outer-loop EXIT, outer-loop CONTINUE, and labeled error counters.
A label is an optional name attached to a PL/pgSQL block (or to a loop) that lets you refer to that specific block from elsewhere in the code. The two main use cases:
- Disambiguating shadowed names. If an inner block declares a variable with the same name as one in the outer block, qualifying as
outer_label.variablereaches the outer one explicitly. - Targeting nested-loop control.
EXIT labelandCONTINUE labelcan break out of (or skip an iteration of) a specific level of nested loops, not just the innermost one.
A label is written as an identifier between double angle brackets, placed immediately before the BEGIN keyword:
The label name after END is optional but recommended — it confirms which block is closing and helps when blocks are nested deeply.
$$, not before. The original tutorial showed <<label>> $$ DECLARE ... $$; — that's wrong. $$ is just a string delimiter; what comes between the dollar signs is the function body, and the label belongs inside that body, immediately before BEGIN (or DECLARE if there's a declarations section). For functions: $$ <<label>> DECLARE ... BEGIN ... END label; $$.An inner block redeclares counter, shadowing the outer one. The label outer on the outer block lets the inner block still reach the outer counter via outer.counter:
DO $$
<>
DECLARE
counter INTEGER := 100;
BEGIN
RAISE NOTICE 'Outer counter starts at %', counter;
DECLARE
counter INTEGER := 1; -- Shadows outer.counter
BEGIN
RAISE NOTICE 'Inner counter: %', counter;
RAISE NOTICE 'Outer counter (via label): %', outer.counter;
outer.counter := outer.counter + 5;
END;
RAISE NOTICE 'Outer counter is now: %', counter;
END outer;
$$;
The inner block both read the outer counter (RAISE NOTICE 'via label') and modified it (outer.counter := outer.counter + 5) — the unqualified name counter inside the inner block always meant the inner one.
With nested loops, plain EXIT exits only the innermost. Labels let you exit a specific outer loop:
DO $$
DECLARE
i INTEGER;
j INTEGER;
BEGIN
<>
FOR i IN 1 .. 5 LOOP
<>
FOR j IN 1 .. 5 LOOP
RAISE NOTICE 'i=%, j=%', i, j;
-- Exit the OUTER loop entirely when i=2, j=3
IF i = 2 AND j = 3 THEN
EXIT outer_loop;
END IF;
END LOOP inner_loop;
END LOOP outer_loop;
RAISE NOTICE 'Done.';
END $$;
When the condition fires at i=2, j=3, control jumps clean out of both loops because EXIT outer_loop targeted the labeled outer one. A plain EXIT would have exited only the inner loop and continued i=2, j=4, i=2, j=5, then proceeded to i=3.
CONTINUE label skips the rest of the current iteration of the named loop and starts its next iteration:
DO $$
DECLARE
i INTEGER;
j INTEGER;
BEGIN
<>
FOR i IN 1 .. 3 LOOP
FOR j IN 1 .. 3 LOOP
-- If j=2, skip to next i (not next j)
IF j = 2 THEN
CONTINUE outer_loop;
END IF;
RAISE NOTICE 'i=%, j=%', i, j;
END LOOP;
END LOOP outer_loop;
END $$;
For each i, the inner loop prints j=1, then hits j=2, and CONTINUE outer_loop jumps directly to the next iteration of the outer loop — skipping not just the rest of the inner iteration but the entire remaining inner loop too.
Labels also help when an exception handler in a nested block needs to refer to outer state:
DO $$
<>
DECLARE
error_count INTEGER := 0;
i INTEGER;
BEGIN
FOR i IN 1 .. 5 LOOP
BEGIN
-- Force a divide-by-zero on i=3 to demonstrate
IF i = 3 THEN
PERFORM 1 / 0;
END IF;
RAISE NOTICE 'Processed item %', i;
EXCEPTION
WHEN division_by_zero THEN
main.error_count := main.error_count + 1;
RAISE NOTICE 'Item % failed (errors so far: %)', i, main.error_count;
END;
END LOOP;
RAISE NOTICE 'Final error count: %', error_count;
END main;
$$;
The exception handler runs inside the inner block but updates main.error_count on the outer one. Without the label, this still works (the inner block can see the outer name), but the label makes the intent explicit — the reader knows immediately that this is "outer state being maintained from a nested handler."
| Construct | Label syntax |
|---|---|
| Block | <<name>> BEGIN ... END name; |
| LOOP | <<name>> LOOP ... END LOOP name; |
| WHILE loop | <<name>> WHILE cond LOOP ... END LOOP name; |
| FOR loop | <<name>> FOR i IN 1..n LOOP ... END LOOP name; |
| FOREACH loop | <<name>> FOREACH x IN ARRAY ... LOOP ... END LOOP name; |
- Place the label immediately before
BEGIN(after$$for top-level functions, or after the parent'sBEGINfor nested blocks). The original tutorial put labels in wrong places — the syntax is fixed:<<label>>goes right whereDECLAREorBEGINwould otherwise begin. - Match labels at
END.END label_name;documents intent and catches mistakes — Postgres will error if the closing label doesn't match the opening one. - Use labels for clarity, not just necessity. Even when not strictly required, a label on a deeply nested block makes the code far easier to follow.
- Use labels for all multi-level loop control —
EXITandCONTINUEwith labels make intent unambiguous; without labels, the reader has to count nested LOOP keywords. - Don't reuse label names across nested levels. Pick distinct names (
outer_loop,inner_loop) so any reference is unambiguous. - Don't put a stray semicolon after BEGIN.
BEGIN;is a syntax error. The original tutorial had this;BEGINis followed directly by the first statement, not a semicolon.
- A label
<<name>>tags a block or loop so it can be referenced elsewhere in the code. - The label goes immediately before
BEGIN(orDECLAREif a declarations section is present) — not before$$. - Use
label.variableto reach a shadowed outer variable from inside a nested block. EXIT labelandCONTINUE labelcontrol nested loops at a specific level — much clearer than counting LOOP keywords.- Label any deeply nested block for code clarity, even when you don't strictly need it.
- Match the label at
END label_name;for self-documenting blocks and to catch mistakes early.