Skip to content

Control Flow

Directing Program Logic

Version: 1.0 Year: 2026


Copyright (c) 2025-2026 Ryan Thomas Robson / Robworks Software LLC. Licensed under CC BY-NC-ND 4.0. You may share this material for non-commercial purposes with attribution, but you may not distribute modified versions.


Conditionals: if/elsif/else

Every program needs to make decisions. In Perl, the primary tool for branching is if:

my $temperature = 95;

if ($temperature > 100) {
    print "Boiling!\n";
} elsif ($temperature > 80) {
    print "Hot\n";
} elsif ($temperature > 60) {
    print "Comfortable\n";
} else {
    print "Cold\n";
}

A few things stand out immediately:

  • The condition must be in parentheses.
  • The body must be in braces, even for a single statement. Perl does not allow braceless if blocks (unlike C or JavaScript).
  • elsif has no second e. This trips up every newcomer exactly once.

Comparison Operators

Perl has two complete sets of comparison operators - one for numbers and one for strings. Using the wrong set is one of the most common Perl bugs.

Operation Numeric String
Equal == eq
Not equal != ne
Less than < lt
Greater than > gt
Less or equal <= le
Greater or equal >= ge
Comparison (three-way) <=> cmp

The three-way operators (<=> and cmp) return -1, 0, or 1. They are essential for custom sort comparators:

my @sorted = sort { $a <=> $b } @numbers;    # numeric ascending
my @alpha  = sort { $a cmp $b } @strings;     # alphabetical

String vs. Numeric Comparison

"apple" == "banana" evaluates to true because == forces both strings to numeric context, converting them to 0. Use eq for string comparison. If use warnings is enabled (and it should be), Perl will warn you about this mistake.

Compound Conditions

Combine conditions with logical operators. Perl provides two styles:

Meaning Symbol (high precedence) Word (low precedence)
AND && and
OR \|\| or
NOT ! not
# Symbol style - common in conditions
if ($age >= 18 && $age <= 65) {
    print "Working age\n";
}

# Word style - common in flow control
open my $fh, '<', $file or die "Cannot open $file: $!";

The word operators (and, or, not) have lower precedence than assignment, which is why open ... or die works without parentheses. The symbol operators (&&, ||, !) bind more tightly and are the standard choice inside conditionals.

Precedence Rule of Thumb

Use &&/||/! inside if conditions. Use and/or/not for flow control at the statement level (like open ... or die). Mixing them in the same expression leads to surprises.


unless

unless is a negated if. It executes the block when the condition is false:

unless ($user_authenticated) {
    redirect_to_login();
}

This reads more naturally than if (!$user_authenticated) in many cases. But there are clear rules about when unless helps readability:

Use unless when:

  • The condition is a simple boolean or single test
  • The English reads naturally: "unless the user is authenticated, redirect"

Avoid unless when:

  • You need an elsif or else branch - unless/else reads backwards and confuses everyone
  • The condition is compound (unless ($a && !$b) is a logic puzzle)
  • You are using double negatives (unless (!$found) - just use if ($found))
# Good - reads naturally
die "File not found" unless -e $filename;

# Bad - unless with else is confusing
unless ($ready) {
    wait_more();
} else {
    proceed();   # Wait, so this runs when $ready is true?
}

# Better - just use if
if ($ready) {
    proceed();
} else {
    wait_more();
}

The Ternary Operator

The ternary operator (?:) is an expression-level if/else. It evaluates to one of two values based on a condition:

my $status = ($count > 0) ? "active" : "empty";

This is equivalent to:

my $status;
if ($count > 0) {
    $status = "active";
} else {
    $status = "empty";
}

The ternary operator shines in assignments, function arguments, and print statements - anywhere you need a value, not a block:

printf "Found %d %s\n", $count, ($count == 1) ? "item" : "items";

my $label = $is_admin ? "Administrator" : "User";

You can nest ternaries, but deep nesting becomes unreadable quickly:

# One level of nesting is sometimes acceptable
my $grade = ($score >= 90) ? "A"
          : ($score >= 80) ? "B"
          : ($score >= 70) ? "C"
          :                  "F";

# More than two levels? Use if/elsif instead.

Tip

If a ternary expression does not fit on one line or requires nesting more than two levels deep, switch to if/elsif. The few saved lines are not worth the readability cost.


Statement Modifiers

Perl supports postfix conditionals - you can put if, unless, while, until, for, or foreach after a single statement:

print "Found it!\n" if $found;
warn "Disk full\n"  unless $space_available;
print $_ while <STDIN>;
$total += $_ for @values;

Statement modifiers work only with a single statement on the left side. You cannot put a block before a modifier:

# This is a syntax error
{
    print "hello\n";
    print "world\n";
} if $greet;

# Do this instead
if ($greet) {
    print "hello\n";
    print "world\n";
}

The common modifiers and when to use them:

Modifier Meaning Example
if Execute when true return $cached if exists $cache{$key};
unless Execute when false die "Required" unless defined $value;
while Loop while true print <$fh> while defined($_ = <$fh>);
until Loop until true sleep 1 until -e $lockfile;
for/foreach Loop over list print "$_\n" for @names;

Statement modifiers read like English and reduce visual clutter for simple operations. The convention is: use the modifier form when the action is more important than the condition, and the whole thing fits on one line.

# Modifier form - emphasizes the action
next if $line =~ /^#/;

# Block form - emphasizes the condition
if ($line =~ /^#/) {
    $comment_count++;
    push @comments, $line;
    next;
}

Perl's Truth Rules

Truth in Perl is simple once you learn the four false values. Understanding truthiness is fundamental to every conditional, loop, and short-circuit operator in the language.

A value is false if it is:

  1. undef - no value at all
  2. "" - the empty string
  3. 0 - the number zero
  4. "0" - the string containing just the character zero

Everything else is true. This includes "0E0", " " (a space), "00", 0.0 (which stringifies to "0"), empty arrays in scalar context (they evaluate to 0), and references (always true).

# All false
if (undef)  { ... }   # false
if ("")     { ... }   # false
if (0)      { ... }   # false
if ("0")    { ... }   # false

# All true (some surprise people)
if ("0E0")  { ... }   # true - non-empty string that isn't "0"
if (" ")    { ... }   # true - contains a space
if ("00")   { ... }   # true - two characters, not the string "0"
if (0.0)    { ... }   # false - 0.0 is numeric 0
if (\0)     { ... }   # true - it's a reference
if ([])     { ... }   # true - it's a reference to an empty array

The \"0E0\" Trick

The DBI module returns "0E0" for queries that succeed but affect zero rows. This evaluates to true in boolean context (it is a non-empty string that is not "0") but to 0 in numeric context (it is zero in scientific notation). This lets you distinguish "zero rows affected" (true, "0E0") from "query failed" (false, undef).

The following diagram shows how Perl evaluates truthiness:

flowchart TD
    A[Value to evaluate] --> B{Is it undef?}
    B -->|Yes| FALSE[FALSE]
    B -->|No| C{Is it a string?}
    C -->|Yes| D{Is it empty ''?}
    D -->|Yes| FALSE
    D -->|No| E{Is it '0'?}
    E -->|Yes| FALSE
    E -->|No| TRUE[TRUE]
    C -->|No| F{Is it 0?}
    F -->|Yes| FALSE
    F -->|No| TRUE

In practice, Perl internally converts between strings and numbers freely, so the evaluation is more nuanced. A value like 0.0 is numeric zero, which is false. A value like "00" is the string "00", not "0", so it is true. The rules above cover every case you will encounter in real code.


while and until Loops

The while loop repeats a block as long as the condition is true:

my $count = 10;
while ($count > 0) {
    print "$count...\n";
    $count--;
}
print "Liftoff!\n";

The until loop is the opposite - it repeats until the condition becomes true:

my $response;
until (defined $response && $response eq "yes") {
    print "Continue? (yes/no): ";
    $response = <STDIN>;
    chomp $response;
}

do-while and do-until

If you need the body to execute at least once before checking the condition, use do:

my $input;
do {
    print "Enter a number (1-10): ";
    $input = <STDIN>;
    chomp $input;
} while ($input < 1 || $input > 10);

do-while Is Not a True Loop

A do { ... } while block is not a real loop in Perl's eyes. The next, last, and redo loop control statements do not work inside it. If you need loop control, use a regular while loop with the exit condition at the end.

Infinite Loops

while (1) creates an infinite loop. You break out of it with last:

while (1) {
    print "Command: ";
    my $cmd = <STDIN>;
    chomp $cmd;
    last if $cmd eq "quit";
    process_command($cmd);
}

Reading Input with while

One of Perl's most common patterns is reading input line by line:

while (my $line = <STDIN>) {
    chomp $line;
    print "Got: $line\n";
}

Perl has special magic here: the while (<STDIN>) construct automatically checks for defined rather than truth, so it correctly handles lines containing just "0". Writing while (defined(my $line = <STDIN>)) is equivalent, but the short form is idiomatic.

The same pattern works for files:

open my $fh, '<', 'data.txt' or die "Cannot open: $!";
while (my $line = <$fh>) {
    chomp $line;
    # process $line
}
close $fh;

for and foreach Loops

Perl has two loop styles: C-style for and list-iteration foreach. In practice, Perl treats for and foreach as interchangeable keywords - you can use either for both styles. Most Perl programmers use for exclusively.

C-style for

for (my $i = 0; $i < 10; $i++) {
    print "$i\n";
}

The three-part header works exactly like C: initialize, test, increment. The loop variable $i is lexically scoped to the loop when declared with my.

List Iteration with for/foreach

The far more common style iterates over a list:

my @fruits = ("apple", "banana", "cherry");

for my $fruit (@fruits) {
    print "I like $fruit\n";
}

If you omit the loop variable, Perl uses $_:

for (@fruits) {
    print "I like $_\n";
}

Aliasing, Not Copying

The loop variable is an alias for the current element, not a copy. Modifying the loop variable modifies the original array:

my @nums = (1, 2, 3);
for (@nums) {
    $_ *= 2;
}
# @nums is now (2, 4, 6)

This is powerful but can cause bugs if you forget about it. Use a named variable (for my $item (@array)) to make the aliasing visible and intentional.

The Range Operator

The range operator (..) generates a list of consecutive values:

for my $n (1..10) {
    print "$n\n";
}

# Also works with characters
for my $letter ('a'..'z') {
    print "$letter ";
}

Reverse Iteration

To iterate in reverse:

for my $i (reverse 1..10) {
    print "$i...\n";
}

Iterating with Index

When you need both the index and the value:

my @items = ("first", "second", "third");

for my $i (0..$#items) {
    print "$i: $items[$i]\n";
}

$#items gives the last index of the array.


Loop Control

Three statements control loop execution from inside the body: next, last, and redo.

Statement Effect Equivalent in C/Python
next Skip to the next iteration continue / continue
last Exit the loop entirely break / break
redo Restart the current iteration (no re-check) No equivalent
for my $n (1..20) {
    next if $n % 2 == 0;    # skip even numbers
    last if $n > 15;         # stop after 15
    print "$n\n";            # prints 1, 3, 5, 7, 9, 11, 13, 15
}

redo

redo restarts the current iteration without re-evaluating the loop condition or advancing the iterator. It is useful for retry logic:

for my $server (@servers) {
    my $response = ping($server);
    unless ($response) {
        warn "Retrying $server...\n";
        sleep 1;
        redo;   # try the same $server again
    }
    print "$server is up\n";
}

Infinite redo

Without a counter or other exit condition, redo can create an infinite loop. Always include a way to eventually stop retrying:

my $attempts = 0;
for my $server (@servers) {
    $attempts = 0;
    RETRY: {
        my $response = ping($server);
        unless ($response) {
            $attempts++;
            redo RETRY if $attempts < 3;
            warn "Giving up on $server\n";
            next;
        }
    }
    print "$server is up\n";
}

Labeled Loops

When you have nested loops, next and last affect the innermost loop by default. Labels let you target an outer loop:

OUTER: for my $i (1..10) {
    for my $j (1..10) {
        next OUTER if $j == 5;    # skip to next $i
        last OUTER if $i == 3;    # exit both loops
        print "$i.$j ";
    }
}

Labels are uppercase by convention. They go before the loop keyword followed by a colon. You can use any label name, but OUTER, LINE, ROW, and FILE are common choices that describe what the loop iterates over.


Short-Circuit Operators

Perl's logical operators do not just return true or false - they return the value that determined the result. This makes them powerful control flow tools beyond simple boolean logic.

|| and && as Value Selectors

The || (logical or) operator evaluates the left side. If true, it returns that value. If false, it evaluates and returns the right side:

my $name = $user_input || "Anonymous";
# If $user_input is truthy, $name gets that value
# If $user_input is false (empty string, undef, 0), $name gets "Anonymous"

The && (logical and) operator evaluates the left side. If false, it returns that value. If true, it evaluates and returns the right side:

my $result = $data && process($data);
# Only calls process() if $data is truthy

The Defined-Or Operator: //

The || operator has a problem: it treats 0 and "" as false, which is often not what you want for defaults. The // (defined-or) operator checks for defined instead of truth:

my $port = $config{port} // 8080;
# Uses 8080 only if $config{port} is undef
# If $config{port} is 0, it keeps 0 (unlike ||)

This is critical for numeric defaults where 0 is a legitimate value:

my $count = $args{count} || 10;    # Bug: count of 0 becomes 10
my $count = $args{count} // 10;    # Correct: only undef becomes 10

Assignment Shortcuts

Both || and // have assignment forms:

$x ||= "default";     # $x = $x || "default"
$x //= "default";     # $x = $x // "default"

$x //= "default" is the standard pattern for setting defaults. It assigns "default" only if $x is undef, leaving 0, "", and other false-but-defined values alone.

Error Handling with or

The or operator (low-precedence version of ||) is Perl's idiom for error handling:

open my $fh, '<', $filename or die "Cannot open $filename: $!";
chdir $directory            or die "Cannot chdir to $directory: $!";
mkdir $path                 or warn "Cannot create $path: $!";

This works because open returns a truthy value on success and false (undef) on failure. When open succeeds, or short-circuits and die never executes. When open fails, or evaluates the right side and die terminates the program with an error message.

or vs || for Error Handling

Use or (not ||) with die/warn because of precedence. open $fh, '<', $file || die ... is parsed as open $fh, '<', ($file || die ...), which passes the result of $file || die as the filename. The or operator's low precedence ensures the open call completes first.


given/when (Experimental)

Perl 5.10 introduced given/when as a switch/case mechanism:

# Historical syntax - DO NOT USE in new code
use feature 'switch';
given ($status) {
    when ("active")   { process_active()   }
    when ("pending")  { process_pending()   }
    when ("inactive") { process_inactive()  }
    default           { handle_unknown()    }
}

Deprecated and Unreliable

given/when has been experimental since its introduction and produces deprecation warnings in Perl 5.38+. The smartmatch operator (~~) that powers it has complex, surprising behavior that even experienced Perl developers find confusing. The feature is expected to be removed in a future Perl release.

Use if/elsif chains instead:

if ($status eq "active") {
    process_active();
} elsif ($status eq "pending") {
    process_pending();
} elsif ($status eq "inactive") {
    process_inactive();
} else {
    handle_unknown();
}

For dispatch tables (mapping values to actions), use a hash of code references:

my %dispatch = (
    active   => \&process_active,
    pending  => \&process_pending,
    inactive => \&process_inactive,
);

my $handler = $dispatch{$status} // \&handle_unknown;
$handler->();

Putting It All Together

Control flow in Perl is about choosing the right tool for each situation:

  • if/elsif/else for multi-way branching
  • unless for simple negated conditions
  • Ternary for inline value selection
  • Statement modifiers for concise single-statement conditions
  • while/until for condition-driven loops
  • for/foreach for iterating over lists
  • next/last/redo for fine-grained loop control
  • Labels for controlling nested loops
  • ||////&& for short-circuit logic and defaults
  • or/and for statement-level flow control and error handling

The key is readability. Perl gives you many ways to express the same logic. Pick the one that makes intent clearest to someone reading your code six months from now - including yourself.


Further Reading


Previous: Arrays, Hashes, and Lists | Next: Regular Expressions | Back to Index

Comments