Tcl Part I · Module 14 · Errors and Debugging
Return Codes: What catch Actually Reports
catch returns a completion code, and almost all code treats it as a boolean — zero means it worked, anything else means it failed. That is right often enough to be a reasonable habit and wrong in a specific, recognisable way. There are five codes, not two, and the three you are not thinking about mean something quite different from an error: a return, break or continue that travelled further than its author intended. Recognising a 2 or a 3 arriving at a catch is one of those diagnostics that turns a baffling bug into an obvious one.
Intermediate11 min readTclPart IErrorsReturn codesDebugging
Part I · Module 14 · Chapter 14.4 · Return Codes: What catch Actually Reports
1. The five codes
foreach {label body} {
ok {set x 1}
error {error "boom"}
return {return "early"}
break {break}
continue {continue}
} {
set rc [catch $body result]
puts [format " %-9s code %d result '%s'" $label $rc $result]
} ok code 0 result '1'
error code 1 result 'boom'
return code 2 result 'early'
break code 3 result ''
continue code 4 result ''| Code | Name | Means |
|---|---|---|
| 0 | TCL_OK | normal completion; the result is a value |
| 1 | TCL_ERROR | an error; the result is the message |
| 2 | TCL_RETURN | a return; the result is the returned value |
| 3 | TCL_BREAK | a break; the result is empty |
| 4 | TCL_CONTINUE | a continue; the result is empty |
2. Why 2, 3 and 4 reach a catch at all
Normally they do not. A break is consumed by the enclosing loop and a return by the enclosing procedure, so they never travel far enough for a catch to see them.
They reach a catch when there is no enclosing construct to consume them — which happens whenever a script is passed around as a value and run somewhere other than where it was written:
set body { break }
puts "run inside a loop:"
foreach x {1 2 3} {
eval $body
puts " never reached"
}
puts " loop ended early, as break should"
puts "run with no loop around it:"
puts " catch reports code [catch {eval $body} r]"run inside a loop:
loop ended early, as break should
run with no loop around it:
catch reports code 3Same script, two contexts. Inside a loop the break was consumed; outside one it kept going until the catch caught it.
3. The bug signature
Anything that takes a body and runs it — the repeat from Chapter 11.5 §4, a retry wrapper, a per-item handler — can be handed a body containing break, continue or return:
proc for_each {items body} {
foreach item $items {
set rc [catch {uplevel 1 $body} result]
if {$rc} {
puts " handler reported code $rc"
}
}
}
puts "a body that returns:"
for_each {a b} { return "done with this one" }
puts "a body that breaks:"
for_each {a b} { break }a body that returns:
handler reported code 2
handler reported code 2
a body that breaks:
handler reported code 3
handler reported code 3Two things are wrong there, and both are the wrapper's fault. The return was the body author's way of saying "finish this item", and the wrapper treated it as a failure. The break was meant to end the iteration, and the wrapper kept going.
4. Handling the codes properly
A wrapper that runs caller-supplied scripts should dispatch on the code:
proc for_each {items body} {
set done 0
foreach item $items {
set rc [catch {uplevel 1 $body} result]
switch -- $rc {
0 { }
1 { return -code error $result }
2 { }
3 { set done 1 }
4 { }
default { return -code $rc $result }
}
if {$done} break
}
return
}
puts "normal body:"
for_each {a b c} { puts " item" }
puts "body with break:"
for_each {a b c} { puts " item" ; break }
puts "body with an error:"
if {[catch {for_each {a b} { error "body failed" }} m]} {
puts " propagated: $m"
}normal body:
item
item
item
body with break:
item
body with an error:
propagated: body failedEach code now means what it meant to the body's author: break stops the iteration, continue and return finish the current item, and an error propagates.
5. return -code produces any code
proc finish_with {code value} {
return -code $code $value
}
foreach c {0 1 2 3 4} {
set rc [catch {finish_with $c "value-$c"} r]
puts [format " asked for %d -> catch saw %d, result '%s'" $c $rc $r]
} asked for 0 -> catch saw 0, result 'value-0'
asked for 1 -> catch saw 1, result 'value-1'
asked for 2 -> catch saw 2, result 'value-2'
asked for 3 -> catch saw 3, result 'value-3'
asked for 4 -> catch saw 4, result 'value-4'Each code passes straight through to the caller. That is how a procedure signals break to a loop it is called from, and it is the general form behind error — which, as Chapter 14.1 §5 showed, is just return -code error with a shorter spelling.
6. What is actually in the options dictionary
The dictionary from catch's third argument does not have the same keys for every code:
foreach {label body} {
error {error "boom"}
break {break}
continue {continue}
return {return "v"}
ok {set x 1}
} {
catch $body r opts
puts [format " %-9s %s" $label [lsort [dict keys $opts]]]
} error -code -errorcode -errorinfo -errorline -level
break -code -level
continue -code -level
return -code -level
ok -code -level-errorcode, -errorinfo and -errorline exist only for errors. Reading them unconditionally fails:
catch {break} r opts
dict get $opts -errorcodekey "-errorcode" not known in dictionarySo any code that inspects the options must check the code first — which is the same discipline as checking catch's return value before reading the result variable.
-level is the other key, and it is how many procedure boundaries the code has still to travel:
catch {error "x"} r opts
puts "error : -code [dict get $opts -code] -level [dict get $opts -level]"
catch {return "v"} r opts
puts "return : -code [dict get $opts -code] -level [dict get $opts -level]"error : -code 1 -level 0
return : -code 0 -level 1An error has level 0 — act here. A return has level 1 — act one frame out, which is what makes it end a procedure rather than the catch.
7. A practical check
When a catch reports something unexpected, print the code and only then the error-specific keys:
proc run_body {body} {
set rc [catch {uplevel 1 $body} result opts]
puts " code : $rc"
puts " result : '$result'"
if {$rc == 1} {
puts " errcode: [dict get $opts -errorcode]"
}
}
puts "an error:" ; run_body { error "real failure" }
puts "a break:" ; run_body { break }
puts "a return:" ; run_body { return "a value" }an error:
code : 1
result : 'real failure'
errcode: NONE
a break:
code : 3
result : ''
a return:
code : 2
result : 'a value'A code of 2 with a sensible-looking "message", or a 3 with an empty one, says immediately that this is not an error.
8. Exercises
Exercise 1. Name the five completion codes and what each means.
Exercise 2. Why do codes 3 and 4 normally never reach a catch?
Exercise 3. What circumstance makes them reach one?
Exercise 4. What is wrong with if {[catch {uplevel 1 $body}]} { ... } in a wrapper that runs caller-supplied scripts?
Exercise 5. Why do -errorcode and -errorinfo sometimes not exist in the options dictionary?
Exercise 6. You see a caught "error" whose message is a value. What is it?
Answers
1. 0 normal completion, 1 error, 2 return, 3 break, 4 continue.
2. Because the enclosing loop consumes them. A break written inside a foreach never travels past that foreach.
3. A script passed around as a value and run somewhere there is no enclosing loop or procedure to consume it — a body given to a wrapper, a callback, anything run with eval or uplevel.
4. It treats a return, break or continue in the caller's body as a failure. Those are legitimate things for a body author to write, and the "error message" printed will be the returned value or an empty string.
5. Because they are recorded only for errors. For codes 0, 2, 3 and 4 the dictionary holds just -code and -level, so reading -errorcode unconditionally raises key "-errorcode" not known in dictionary. Check the code first.
6. Almost certainly completion code 2 — a return from a body that escaped to your catch, with the returned value showing up where you expected a message. Print the code to confirm.
9. Remember
- Five codes: 0 ok, 1 error, 2 return, 3 break, 4 continue.
- 2, 3 and 4 normally never reach a
catch— the enclosing procedure or loop consumes them. - They reach one when a script is run away from where it was written — a caller-supplied body, a callback, an
uplevel. if {[catch ...]}treats them all as failure, which is a bug in any wrapper that runs caller-supplied scripts.- Dispatch on the code with
switchin such a wrapper: propagate 1, let 3 stop the iteration, treat 2 and 4 as finishing the item. - Re-raise with
return -code error $result, not a fresherror. return -code Nfrom a procedure arrives as N; written directly in a caught script with no procedure boundary it arrives as 2.-errorcodeand-errorinfoexist only for errors — check the code before reading them.- An "error" whose message is not an error message is usually a code 2.
Where this fits
Part of the Tcl curriculum.
