Skip to content
VLSI Mentor

Tcl Part I · Module 10 · Procedures and Reuse

Guard Clauses and Early Return

You have met this shape twice already — flattening nested conditions in Module 8, and skipping uninteresting loop passes in Module 9. At a procedure boundary it becomes something more than a readability technique, because a procedure can leave in two different ways. It can return a neutral value, which tells the caller there was nothing to do, or it can raise an error, which tells the caller it was asked something it could not answer. Those are different contracts, callers handle them differently, and choosing between them for each guard is the part of this that takes judgement rather than typing.

Foundation11 min readTclPart IProceduresGuard clausesValidation

Part I · Module 10 · Chapter 10.7 · Guard Clauses and Early Return

1. The shape

A procedure that validates by nesting puts its real work at the bottom of a staircase:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc summarise_nested {rows} {
    if {[llength $rows] > 0} {
        if {[llength $rows] % 2 == 0} {
            set n 0
            foreach {k v} $rows { incr n }
            return "$n entries"
        } else {
            return "error: not pairs"
        }
    } else {
        return "nothing to report"
    }
}

The same procedure with guards puts each rejection next to its reason and leaves the work unindented:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc summarise {rows} {
    if {[llength $rows] == 0}      { return "nothing to report" }
    if {[llength $rows] % 2 != 0}  { return "error: not pairs" }

    set n 0
    foreach {k v} $rows { incr n }
    return "$n entries"
}

Check they agree before trusting the rewrite — the discipline from Chapter 8.3 §2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach input {{} {a 1 b 2} {a 1 b}} {
    set x [summarise_nested $input]
    set y [summarise $input]
    puts [format "%-12s %-18s %s" "'$input'" $y [expr {$x eq $y ? "(same)" : "DIFFERS"}]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
''           nothing to report  (same)
'a 1 b 2'    2 entries          (same)
'a 1 b'      error: not pairs   (same)

2. A guard has two possible exits

This is what is new at a procedure boundary. Each guard must decide: is this a case where the caller can sensibly carry on, or one where it must not?

Return a neutral value when "nothing to do" is a legitimate answer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc summarise2 {rows} {
    if {[llength $rows] == 0} { return "nothing to report" }
    return "[expr {[llength $rows] / 2}] entries"
}
puts [summarise2 {}]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nothing to report

Raise when the input is wrong rather than empty:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc summarise3 {rows} {
    if {[llength $rows] % 2 != 0} {
        error "rows must be name-value pairs, got [llength $rows] elements"
    }
    return "[expr {[llength $rows] / 2}] entries"
}

if {[catch {summarise3 {a 1 b}} msg]} {
    puts "rejected: $msg"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rejected: rows must be name-value pairs, got 3 elements

An empty list is a reasonable thing for a caller to hold; an odd-length pair list is a bug upstream. The first deserves an answer, the second deserves to stop.

3. Order matters

Guards are not an unordered set. Later ones may depend on earlier ones having passed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc scale_safe {value factor} {
    if {![string is double -strict $value]}  { error "value is not a number: '$value'" }
    if {![string is double -strict $factor]} { error "factor is not a number: '$factor'" }
    if {$factor == 0}                        { error "factor must not be zero" }

    return [expr {$value / $factor}]
}

puts [scale_safe 10 2]
foreach bad {{abc 2} {10 xyz} {10 0}} {
    lassign $bad v f
    if {[catch {scale_safe $v $f} m]} { puts "  rejected: $m" }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
5
  rejected: value is not a number: 'abc'
  rejected: factor is not a number: 'xyz'
  rejected: factor must not be zero

The zero check has to come after the type checks, because $factor == 0 on the text xyz would be a string comparison rather than the numeric test intended — the behaviour from Chapter 8.3 §5. Reorder them and the third message stops being reachable for some inputs.

4. Guards state the contract Tcl cannot check

Chapter 10.2 showed Tcl checking argument counts for free. It checks nothing else — not types, not ranges, not structure. Guards are where you say the rest:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc set_width {name width} {
    if {$name eq ""}                        { error "name must not be empty" }
    if {![string is integer -strict $width]} { error "width must be an integer, got '$width'" }
    if {$width < 1 || $width > 1024}        { error "width must be 1..1024, got $width" }

    return "$name is $width bits"
}

puts [set_width clk 1]
foreach bad {{"" 8} {clk abc} {clk 0} {clk 99999}} {
    lassign $bad n w
    if {[catch {set_width $n $w} m]} { puts "  rejected: $m" }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clk is 1 bits
  rejected: name must not be empty
  rejected: width must be an integer, got 'abc'
  rejected: width must be 1..1024, got 0
  rejected: width must be 1..1024, got 99999

Each message names the rule and the offending value. That is the same quality of diagnostic wrong # args gives, extended to the things Tcl has no way of knowing.

5. Do not over-guard

Validation has a cost: every guard is a line a reader must check, and a line that can itself be wrong. Two rules keep it proportionate.

Guard at the boundary, not everywhere. A procedure that is only ever called by two others in the same file does not need to re-validate what they already checked:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# public entry point — validates
proc process {rows} {
    if {[llength $rows] % 2 != 0} { error "rows must be pairs" }
    return [process_checked $rows]
}

# internal — trusts its caller
proc process_checked {rows} {
    set n 0
    foreach {k v} $rows { incr n }
    return "$n entries"
}

puts [process {a 1 b 2}]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2 entries

Do not guard against things that already fail well. dict get on a missing key already raises a clear, specific error naming the key; wrapping it in a dict exists check that raises a vaguer message makes the diagnostic worse, not better.

6. Guards and the implicit return

One interaction worth naming. Guards make it easy to end up with a path that returns nothing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc risky {n} {
    if {$n < 0} { return "negative" }
    if {$n > 100} { return "too large" }
    # falls off the end for 0..100
}

puts "neg : '[risky -1]'"
puts "big : '[risky 200]'"
puts "mid : '[risky 50]'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
neg : 'negative'
big : 'too large'
mid : ''

The guards handled the exceptional cases and nobody wrote the normal one. This is Chapter 10.5 §4's rule arriving through the back door: with guards at the top, the final return is easy to forget precisely because the body looks finished.

7. A worked example

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc parse_port {spec} {
    # guards: reject, with a reason
    if {[llength $spec] != 3} {
        error "a port spec needs 3 fields, got [llength $spec]: '$spec'"
    }
    lassign $spec name dir width

    if {$name eq ""}                         { error "port name must not be empty" }
    if {$dir ni {in out inout}}              { error "bad direction '$dir' for port $name" }
    if {![string is integer -strict $width]} { error "width must be an integer for port $name, got '$width'" }
    if {$width < 1}                          { error "width must be at least 1 for port $name, got $width" }

    # the work
    return [dict create name $name dir $dir width $width bits $width]
}

foreach spec {{clk in 1} {data out 32} {bad sideways 8} {oops in} {x in 0}} {
    if {[catch {parse_port $spec} r]} {
        puts "  rejected: $r"
    } else {
        puts "  ok: [dict get $r name] ([dict get $r dir], [dict get $r width] bits)"
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ok: clk (in, 1 bits)
  ok: data (out, 32 bits)
  rejected: bad direction 'sideways' for port bad
  rejected: a port spec needs 3 fields, got 2: 'oops in'
  rejected: width must be at least 1 for port x, got 0

Five guards, each naming its rule and the offending value, and then four lines of actual work at zero indentation.

8. Exercises

Exercise 1. Rewrite with guards and verify both versions agree:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc f {l} {
    if {[llength $l] > 0} {
        return [lindex $l 0]
    } else {
        return ""
    }
}

Exercise 2. For each, say return-a-value or raise: (a) an empty list to a summariser; (b) a negative width; (c) a missing optional label; (d) a dictionary with no name key where name is required.

Exercise 3. Why must a type guard come before a range guard?

Exercise 4. What does risky 50 return in section 6, and what is missing?

Exercise 5. Write average guarding against a non-list, an empty list, and a non-numeric element, then returning the mean.

Exercise 6. When is it better not to add a guard?

Answers

1.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc g {l} {
    if {[llength $l] == 0} { return "" }
    return [lindex $l 0]
}

foreach input {{} {a b} {x}} {
    puts [format "%-8s %s" "'$input'" [expr {[f $input] eq [g $input] ? "same" : "DIFFERS"}]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
''       same
'a b'    same
'x'      same

2. (a) return a value — an empty input is a legitimate thing to summarise and "nothing to report" is a sensible answer; (b) raise — a negative width is a bug upstream; (c) return a value, using the empty default; (d) raise — a required field is missing, so there is no answer to give.

3. Because a range comparison on a non-numeric value does not fail — Tcl falls back to string comparison, so $w < 1 on the text abc produces a boolean rather than an error and the guard silently answers the wrong question. The type guard establishes the precondition the range guard depends on.

4. It returns the empty string. The normal case — a value between 0 and 100 — has no return at all, so the procedure falls off the end.

5.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc average {values} {
    if {[llength $values] == 0} { error "cannot average an empty list" }
    foreach v $values {
        if {![string is double -strict $v]} { error "not a number: '$v'" }
    }
    set total 0.0
    foreach v $values { set total [expr {$total + $v}] }
    return [expr {$total / [llength $values]}]
}

puts [average {1 2 3 4}]
foreach bad {{} {1 x 3}} {
    if {[catch {average $bad} m]} { puts "  rejected: $m" }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2.5
  rejected: cannot average an empty list
  rejected: not a number: 'x'

6. When the thing you would guard against already fails clearly on its own — dict get on a missing key names the key — or when the procedure is internal and its only callers have already validated. A guard that replaces a specific error with a vaguer one makes debugging harder.

9. Remember

  • Validate at the top and leave immediately, so the body holds the work rather than the exceptions.
  • A guard has two exits: return a neutral value, or raise. Choose per guard.
  • Return a value when "nothing to do" is a legitimate answer; raise when the input is wrong.
  • Order guards so that each one's precondition is already established — type checks before range or arithmetic checks.
  • Guards state the contract Tcl cannot check — types, ranges, structure. Tcl checks only argument counts.
  • Use -strict on every string is guard, or the empty string passes.
  • Name the rule and the offending value in each message.
  • Guard at boundaries, not everywhere, and not against things that already fail well.
  • Remember the final return — with guards at the top, the normal path is easy to leave unwritten.

Where this fits

Part of the Tcl curriculum.