Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ok        code 0  result '1'
  error     code 1  result 'boom'
  return    code 2  result 'early'
  break     code 3  result ''
  continue  code 4  result ''
CodeNameMeans
0TCL_OKnormal completion; the result is a value
1TCL_ERRORan error; the result is the message
2TCL_RETURNa return; the result is the returned value
3TCL_BREAKa break; the result is empty
4TCL_CONTINUEa 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
run inside a loop:
  loop ended early, as break should
run with no loop around it:
  catch reports code 3

Same 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a body that returns:
    handler reported code 2
    handler reported code 2
a body that breaks:
    handler reported code 3
    handler reported code 3

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
normal body:
    item
    item
    item
body with break:
    item
body with an error:
    propagated: body failed

Each 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
catch {break} r opts
dict get $opts -errorcode
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
key "-errorcode" not known in dictionary

So 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error  : -code 1  -level 0
return : -code 0  -level 1

An 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 switch in 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 fresh error.
  • return -code N from a procedure arrives as N; written directly in a caught script with no procedure boundary it arrives as 2.
  • -errorcode and -errorinfo exist 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.