Tcl Part I · Module 10 · Procedures and Reuse
return, and the Value a Procedure Produces
A procedure always produces a value, whether or not you asked it to. When you write return, that is the value; when you do not, Tcl hands back whatever the last command in the body happened to produce — and that is the empty string far more often than people expect, because puts produces nothing and an if whose branch was not taken produces nothing either. This chapter settles the rule, shows the three shapes return takes, and argues for a habit that removes the whole class of bug where a procedure silently returns nothing and the caller carries on with it.
Foundation10 min readTclPart IProceduresreturnValues
Part I · Module 10 · Chapter 10.5 · return, and the Value a Procedure Produces
1. Returning explicitly
proc classify {n} {
if {$n < 0} {
return "negative"
}
return "non-negative"
}
puts [classify -5]
puts [classify 5]negative
non-negativereturn ends the procedure immediately and supplies the result. Everything after it in the body is not executed.
With no argument, it returns the empty string:
proc quiet {} {
return
}
puts "'[quiet]'"''2. The implicit result
Without a return, a procedure gives back the result of its last command:
proc implicit {} {
set x 5
expr {$x * 2}
}
puts [implicit]10That is convenient for short procedures and it has a sharp edge, because many commands produce nothing. Chapter 10.1 §4 showed the cases; here they are together:
proc endsSet {} { set y 7 }
proc endsPuts {} { puts -nonewline "" }
proc endsEmpty {} { }
proc endsIf {n} { if {$n > 0} { return "positive" } }
puts "set : '[endsSet]'"
puts "puts : '[endsPuts]'"
puts "empty : '[endsEmpty]'"
puts "if yes: '[endsIf 5]'"
puts "if no : '[endsIf -5]'"set : '7'
puts : ''
empty : ''
if yes: 'positive'
if no : ''Only the first and fourth lines produced a value. set returns what it assigned, so endsSet gives 7. The other three give the empty string: puts returns nothing, an empty body has no last command, and an if whose branch was not taken produces nothing either.
3. Returning early
return works from anywhere in the body, including inside loops and switch:
proc find_port {ports want} {
foreach p $ports {
if {$p eq $want} {
return "found $p"
}
}
return "not found"
}
puts [find_port {clk rst data} data]
puts [find_port {clk rst} data]found data
not foundIt leaves the procedure, not just the loop — the distinction Chapter 9.10 §4 relied on to escape nested loops in one step. The same applies inside a switch:
proc runs_for {mode} {
switch -- $mode {
fast { return 1 }
full { return 10 }
}
return 0
}
foreach m {fast full other} {
puts "$m -> [runs_for $m]"
}fast -> 1
full -> 10
other -> 0That final return 0 is doing real work: without it, an unmatched switch would fall through to the end of the body and the procedure would return the empty string rather than 0.
4. Return on every path
The habit that removes the whole problem: if a procedure is supposed to produce a value, make every path end in an explicit return.
# fragile — the third case returns "" silently
proc grade_bad {n} {
if {$n >= 90} { return A }
if {$n >= 80} { return B }
}
# solid — every path is explicit
proc grade {n} {
if {$n >= 90} { return A }
if {$n >= 80} { return B }
return F
}
puts "bad : '[grade_bad 95]' '[grade_bad 50]'"
puts "good: '[grade 95]' '[grade 50]'"bad : 'A' ''
good: 'A' 'F'5. Returning an error instead of a value
Sometimes the honest answer is that there is no value. error raises, and the caller deals with it:
proc must_be_positive {n} {
if {$n <= 0} {
error "expected a positive number, got '$n'"
}
return $n
}
puts [must_be_positive 5]
if {[catch {must_be_positive -1} msg]} {
puts "rejected: $msg"
}5
rejected: expected a positive number, got '-1'return -code error does the same thing and lets you set the message in the same command:
proc also_fails {n} {
if {$n <= 0} {
return -code error "not positive: '$n'"
}
return $n
}
if {[catch {also_fails 0} msg]} {
puts "rejected: $msg"
}rejected: not positive: '0'For ordinary validation, error reads better and is what you will see most. return -code matters when you are writing something that has to control exactly how it finishes — Module 14 covers that properly.
6. A worked example
A parser that returns a value, nothing, or an error, deliberately:
proc parse_setting {line} {
set t [string trim $line]
# a blank or comment line is legitimately nothing
if {$t eq ""} { return "" }
if {[string match "#*" $t]} { return "" }
# anything else must be a setting
if {![string match "*=*" $t]} {
error "not a setting: '$t'"
}
set i [string first "=" $t]
set key [string trim [string range $t 0 [expr {$i - 1}]]]
set value [string trim [string range $t [expr {$i + 1}] end]]
return [list $key $value]
}
foreach line {"width = 32" "# a note" "" "nonsense"} {
if {[catch {parse_setting $line} r]} {
puts " ERROR $r"
} elseif {$r eq ""} {
puts " skip '$line'"
} else {
puts " ok [lindex $r 0] -> [lindex $r 1]"
}
} ok width -> 32
skip '# a note'
skip ''
ERROR not a setting: 'nonsense'Three outcomes, three mechanisms, and the caller can tell them apart — which it could not if every failure returned an empty string.
7. Exercises
Exercise 1. What does a procedure return if it has no return and its last command is puts "done"?
Exercise 2. What does return with no argument give back?
Exercise 3. What is wrong with this, and when does it show up?
proc sign {n} {
if {$n > 0} { return "+" }
if {$n < 0} { return "-" }
}Exercise 4. Write first_even returning the first even number in a list, or the empty string if there is none.
Exercise 5. When should a procedure raise an error rather than return an empty string?
Exercise 6. Why does the final return 0 matter in the runs_for example in section 3?
Answers
1. The empty string. puts prints its argument and returns nothing, so the procedure's implicit result is nothing.
2. The empty string.
3. Zero takes neither branch, so sign 0 falls off the end and returns the empty string instead of something meaningful. It shows up the first time the input is exactly zero — which, for a function about signs, is both the most interesting case and the one least likely to be in the first test. Add return "0" or similar as a final explicit path.
4.
proc first_even {values} {
foreach v $values {
if {$v % 2 == 0} { return $v }
}
return ""
}
puts "'[first_even {1 3 4 5}]'"
puts "'[first_even {1 3 5}]'"'4'
''5. When carrying on would be wrong. An empty string means "the answer is nothing" and the caller can reasonably proceed; an error means "there is no answer" and the caller must not proceed as though there were. Returning empty for a genuine failure converts a loud problem into a silent one.
6. Because a switch that matches nothing produces the empty string, and without the final return 0 that empty string would become the procedure's result. The call runs_for other would yield '' instead of 0, and arithmetic on it would then fail somewhere else.
8. Remember
return valueends the procedure immediately and supplies the result;returnalone gives the empty string.- Without a
return, the result is the last command's result — which isset's assigned value, or nothing at all fromputs, an empty body, or an untakenif. - An untaken
ifat the end of a body is the commonest cause of a procedure silently returning nothing. returnleaves the procedure, not just an enclosing loop orswitch— which is how you escape nested loops in one step.- Give every path an explicit
returnonce the body has branches. The implicit result is fine only for a single-expression procedure. - A
switchwith no match produces the empty string, so a procedure ending in one needs a fallbackreturn. errorandreturn -code errorraise instead of returning. Use them when carrying on would be wrong.- Empty string and error are different contracts: "the answer is nothing" versus "there is no answer".
Where this fits
Part of the Tcl curriculum.
