perl.gg / hidden-gems

<!-- category: hidden-gems -->

last LABEL on Bare Blocks

2026-04-23

You have a multi-step process. Step 3 fails. You need to bail out, but not from a loop. There is no loop. Just a sequence of steps that should stop early when something goes wrong.

The usual approach: flag variables.

my $ok = 1; if ($ok) { $ok = validate_input($data); } if ($ok) { $ok = check_permissions($user); } if ($ok) { $ok = write_record($data); }
Ugly. Nested. Every step checks the same flag. It's a loop that isn't a loop, pretending it doesn't want to be a loop.

Perl has a better answer. Label a bare block and last out of it.

PROCESS: { validate_input($data) or last PROCESS; check_permissions($user) or last PROCESS; write_record($data) or last PROCESS; say "All steps completed"; }
Clean. Flat. No flag variables. 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 { ... }. No if, no while, no for in front of it. It runs once and that's it.
{ say "I run exactly once"; }
What makes bare blocks interesting is that Perl treats them like single-iteration loops for the purposes of 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 exits the block immediately, just like it would exit a while or for loop. The block is a loop that runs once. last breaks out of it early.

Part 2: ADDING LABELS

Any block in Perl can have 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're exiting.

Part 3: STRUCTURED EARLY EXIT

This is the real payoff. Consider a function that needs to do multiple things in sequence, bailing 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. Any one can fail. When one does, execution drops to the end of the STEPS block and hits the return statement. No nested conditionals. No flag variable threading through everything.

The code reads top to bottom. Each step either succeeds and continues or 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. 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 is independent. When one fails, the block ends cleanly. No deeply nested if/elsif/else chains. No boolean accumulator.

Part 5: COMPARISON TO GOTO

Some people see last LABEL and think "that's just goto with extra steps." It's not.

goto can jump anywhere. Forward, backward, into a different scope, out of a function. It's an unstructured jump. You can create spaghetti code that 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. Always forward. Always outward. Never 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, but 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 2 processing but continues with the rest of CONFIG. last CONFIG bails out of everything. Two labels, two exit points, both perfectly clear about where they go.

Part 7: CLEANER THAN FLAG VARIABLES

Let's 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. No $ok variable bouncing around. No repeated if ($ok) checks. The labeled block version says exactly what it means: do these steps, bail if any 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. But they behave 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. No while needed. You built a loop from a non-loop construct.

next on a bare block is the same as last. It ends the current iteration, and since there's only one iteration, it exits the block. There is no "next iteration" to go 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's 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's no config file, defaults stand. If the file can't be read, defaults stand with a warning. If the JSON is broken, same thing. Only if everything works do the custom settings get applied.

Every failure path is explicit. Every last CUSTOM goes to the same place. The defaults are always 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. Execution continues right after the closing brace. Clean, readable, debuggable.

It's not a hack. It's not an abuse of the language. Perl explicitly supports loop control on bare blocks. The documentation says so. perldoc perlsyn calls bare blocks "loop-like" and says 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. Your future self will thank you when reading code that bails out of a ten-step process at step three and you can see exactly where execution goes next.

perl.gg