◈ Netsurfer / perl.gg / last LABEL on Bare Blocks

last LABEL on Bare Blocks

2026-04-23

This page shows how to use a labeled bare block with last to run a sequence of steps and stop early when any step fails. You will get flat code without flag variables and without nested conditionals.

You usually face a multi-step process where step three fails. You need to bail out, but there is no loop to break from. The sequence just needs to stop early when something goes wrong.

The usual approach uses flag variables.

my $ok = 1; if ($ok) { $ok = validate_input($data); } if ($ok) { $ok = check_permissions($user); } if ($ok) { $ok = write_record($data); }
That code gets ugly fast. Every step checks the same flag, and the nesting grows with each addition. It looks like a loop that refuses to admit it is a loop.

Perl has a cleaner answer. You label a bare block and last out of it when a step fails.

PROCESS: { validate_input($data) or last PROCESS; check_permissions($user) or last PROCESS; write_record($data) or last PROCESS; say "All steps completed"; }
The result is flat and easy to read. No flag variables travel through the function, and execution jumps straight to the end of the block when any step fails.

Part 1: BARE BLOCKS AS LOOP-LIKE CONSTRUCTS

A bare block in Perl is just { ... }. It has no if, while, or for in front of it, and it runs exactly once.
{ say "I run exactly once"; }
What makes bare blocks useful is that Perl treats them like single-iteration loops for loop control. You can use last, next, and redo inside them.
{ say "start"; last; say "this never runs"; } say "after the block";
Output:
start after the block
The last statement exits the block immediately, just like it would exit a while or for loop. The block is a loop that runs once, and last breaks out of it early.

Part 2: ADDING LABELS

Any block in Perl can carry a label. Labels are uppercase identifiers followed by a colon.
SETUP: { say "in the setup block"; }
The label gives the block a name. Without a label, last exits the innermost enclosing block. With a label, last LABEL exits the specific named block, no matter how deeply nested you are.
OUTER: { say "outer start"; INNER: { say "inner start"; last OUTER; say "inner end (never runs)"; } say "outer end (never runs either)"; } say "after everything";
Output:
outer start inner start after everything
last OUTER jumped out of both blocks. Labels give you precise control over which block you are exiting.

Part 3: STRUCTURED EARLY EXIT

This is the practical payoff. Consider a function that needs to run multiple steps in sequence and bail out if any step fails.
sub process_order { my ($order) = @_; my $result = { success => 0 }; STEPS: { my $customer = lookup_customer($order->{customer_id}) or last STEPS; my $inventory = check_inventory($order->{items}) or last STEPS; my $payment = charge_payment($customer, $order->{total}) or last STEPS; my $shipment = create_shipment($order, $inventory) or last STEPS; send_confirmation($customer, $shipment) or last STEPS; $result->{success} = 1; $result->{shipment_id} = $shipment->{id}; } return $result; }
Five steps sit in one block. Any one of them can fail, and when it does, execution drops to the end of the STEPS block and hits the return statement. You avoid nested conditionals and flag variable threading.

The code reads top to bottom. Each step either succeeds and continues, or it bails out. You can see the entire flow at a glance.

Part 4: MULTI-STEP VALIDATION

Validation is where this pattern really shines. You have a form submission with six fields, and each field needs its own validation logic. If any field fails, you want to stop and report the error.
sub validate_form { my ($form) = @_; my @errors; VALIDATE: { if (!$form->{email} || $form->{email} !~ m~\S+\@\S+\.\S+~) { push @errors, "Invalid email"; last VALIDATE; } if (!$form->{name} || length($form->{name}) < 2) { push @errors, "Name too short"; last VALIDATE; } if (!$form->{age} || $form->{age} !~ m~^\d+$~ || $form->{age} < 18) { push @errors, "Must be 18 or older"; last VALIDATE; } if (!$form->{password} || length($form->{password}) < 8) { push @errors, "Password must be at least 8 characters"; last VALIDATE; } } return @errors ? \@errors : undef; }
Each validation step stays independent. When one fails, the block ends cleanly. You avoid deeply nested if/elsif/else chains and boolean accumulators.

Part 5: COMPARISON TO GOTO

Some people see last LABEL and think it is just goto with extra steps. It is not.

goto can jump anywhere. It can jump forward, backward, into a different scope, or out of a function. It is an unstructured jump that can create spaghetti code which is genuinely impossible to follow.

# DON'T DO THIS goto RETRY if $failed; # where is RETRY? could be anywhere
last LABEL can only do one thing: exit a named block. It jumps to the statement immediately after the closing brace of that block. It always moves forward and always stays outward. It never crosses into a different scope.
goto last LABEL - jump anywhere - exit a named block - any direction - always forward/outward - any scope - same or enclosing scope - spaghetti potential - structured exit - hard to reason about - trivially clear
last LABEL is structured control flow. goto is chaos with a name tag.

Part 6: NESTED BLOCK EXITS

The real power shows up when you have blocks inside blocks. Consider a configuration loader that needs to process multiple sections, where each section has its own multi-step validation.
sub load_config { my ($file) = @_; my %config; CONFIG: { open my $fh, '<', $file or last CONFIG; my $raw = do { local $/; <$fh> }; close $fh; PARSE: { my $data = eval { decode_json($raw) }; last CONFIG unless $data; # jump all the way out $data->{version} && $data->{version} >= 2 or last PARSE; # skip to end of PARSE only # version 2+ specific processing $config{features} = $data->{features} // {}; } # code here runs even if PARSE was exited early $config{name} = $data->{name} // 'default'; } return \%config; }
last PARSE skips the version two processing but continues with the rest of CONFIG. last CONFIG bails out of everything. Two labels, two exit points, and both are perfectly clear about where they go.

Part 7: CLEANER THAN FLAG VARIABLES

Let us compare the two approaches side by side for a real scenario. Processing a user registration.

Flag variable approach:

sub register_user { my ($params) = @_; my $ok = 1; my $user; if ($ok) { $ok = !user_exists($params->{email}); } if ($ok) { $ok = validate_password($params->{password}); } if ($ok) { $user = create_user($params); $ok = defined $user; } if ($ok) { $ok = send_welcome_email($user); } return $ok ? $user : undef; }
Labeled block approach:
sub register_user { my ($params) = @_; my $user; REGISTER: { last REGISTER if user_exists($params->{email}); last REGISTER unless validate_password($params->{password}); $user = create_user($params) or last REGISTER; send_welcome_email($user) or last REGISTER; } return $user; }
Same logic, half the code, and no $ok variable bouncing around. You avoid repeated if ($ok) checks. The labeled block version says exactly what it means: run these steps, and bail if any of them fails.

Part 8: REDO AND NEXT ON BARE BLOCKS

Since Perl treats bare blocks as single-iteration loops, redo and next work on them too. They behave slightly differently than you might expect.

redo restarts the block from the top:

my $attempts = 0; RETRY: { $attempts++; say "Attempt $attempts"; my $result = try_connection(); if (!$result && $attempts < 3) { redo RETRY; # go back to the top of the block } } say "Finished after $attempts attempts";
This creates a retry loop from a bare block. redo jumps to the opening brace, and you build a loop from a non-loop construct without a while statement.

next on a bare block does the same thing as last. It ends the current iteration, and since there is only one iteration, it exits the block. There is no next iteration to move to.

{ next; # same as last for a bare block say "never reached"; }
So next on a bare block is legal but pointless. Stick with last for clarity.

Part 9: COMPLEX INITIALIZATION

Here is a pattern from real-world code. Setting up a complex configuration where you want defaults if any part of the setup fails.
my %app_config = ( port => 8080, host => 'localhost', workers => 4, log_file => '/tmp/app.log', ); CUSTOM: { my $file = $ENV{APP_CONFIG} or last CUSTOM; open my $fh, '<', $file or do { warn "Cannot read $file: $!"; last CUSTOM; }; my $json = do { local $/; <$fh> }; close $fh; my $custom = eval { decode_json($json) } or do { warn "Invalid JSON in $file: $@"; last CUSTOM; }; # overlay custom settings onto defaults for my $key (keys %$custom) { $app_config{$key} = $custom->{$key}; } say "Loaded custom config from $file"; } # %app_config has defaults, possibly overridden by custom settings
If there is no config file, the defaults stand. If the file cannot be read, the defaults stand with a warning. If the JSON is broken, the same thing happens. Only when everything works do the custom settings get applied.

Every failure path stays explicit. Every last CUSTOM goes to the same place, and the defaults stay safe.

Part 10: THE TAKEAWAY

last LABEL on bare blocks is one of Perl's most underused features. It gives you structured early exit without loops, without flag variables, and without goto.

The pattern is simple:

LABEL: { step_one() or last LABEL; step_two() or last LABEL; step_three() or last LABEL; # all steps passed }
Label the block, last out when you need to, and execution continues right after the closing brace. The result is clean, readable, and debuggable.

It is not a hack, and it is not an abuse of the language. Perl explicitly supports loop control on bare blocks, and perldoc perlsyn calls bare blocks "loop-like" while confirming that last, next, and redo all work on them.

FLAG VARIABLES LABELED BLOCKS +--------------+ +---------------+ | $ok = 1; | | STEPS: | | if ($ok) { | | { | | $ok = f(); | | f() or last;| | } | | g() or last;| | if ($ok) { | | h() or last;| | $ok = g(); | | } | | } | +---------------+ | if ($ok) { | | | $ok = h(); | "Reads like a | } | checklist." +--------------+ | "Reads like a tax return." .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
Use it, and your future self will thank you when reading code that bails out of a ten-step process at step three. You will see exactly where execution goes next.

perl.gg

✓ Document: Donehidden-gems / 17.1 KiB▧