Tcl Part I · Module 14 · Errors and Debugging
Tcl's Error Model
You have been using catch since Module 7 and error since Module 10 without either being explained. This module pays that debt, and it starts with the idea that makes everything else fall into place: Tcl does not have a separate exception system bolted onto a normal one. Every command finishes with a completion code, and an error is simply a non-zero code that keeps unwinding the call stack until something handles it. The remarkable part is that return, break and continue work the same way, with different codes — so once you understand one of them you understand all four.
Foundation11 min readTclPart IErrorscatchControl flow
Part I · Module 14 · Chapter 14.1 · Tcl's Error Model
1. A command finishes with a code
Every command completes with a completion code. Normal completion is code 0, and almost everything you have written so far has used it without needing a name for it.
An error is code 1:
proc f {} {
error "bad input"
}
f
puts "this line never runs"bad input
while executing
"error "bad input""
(procedure "f" line 2)
invoked from within
"f"
(file "u1.tcl" line 5)The script stopped — this line never runs was never printed. f finished with code 1, nothing handled it, and the interpreter gave up.
The first line is the message. Everything after it is the stack trace Tcl kept, showing the path from the script file down to the error command itself. It goes to standard error, not standard output. Chapter 14.6 is devoted to reading it; for now, just note that Tcl records where a failure came from without being asked.
2. Raising unwinds the stack
The code propagates outward through every enclosing call until something deals with it:
proc inner {} { error "deep failure" }
proc middle {} { inner ; puts "middle continues" }
proc outer {} { middle ; puts "outer continues" }
outer
puts "top level continues"deep failure
while executing
"error "deep failure" "
(procedure "inner" line 1)
invoked from within
"inner "
(procedure "middle" line 1)
invoked from within
"middle "
(procedure "outer" line 1)
invoked from within
"outer"
(file "u2.tcl" line 5)None of the three "continues" lines printed. The error left inner, then middle, then outer, then the script — unwinding each frame as it went.
Read the trace from the bottom up and it is that unwinding, written down: the script file called outer, which called middle, which called inner, which raised. Tcl built that list by appending one frame as the error passed out through each call.
3. catch stops the unwinding
catch runs a script and reports its completion code instead of letting it propagate:
proc inner {} { error "deep failure" }
proc middle {} { inner ; return "middle finished" }
if {[catch {middle} result]} {
puts "caught: $result"
} else {
puts "ok: $result"
}
puts "the script continues"caught: deep failure
the script continuesThe unwinding stopped at the catch. Chapter 14.3 covers it properly; here it is only the thing that interrupts the model.
4. The same mechanism carries return, break and continue
This is the part worth seeing. catch reports the completion code, and there are five of them:
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 | Means | Produced by |
|---|---|---|
| 0 | normal completion | anything that finishes |
| 1 | error | error, or any failing command |
| 2 | return | return |
| 3 | break | break |
| 4 | continue | continue |
So return from a procedure, break out of a loop and error out of everything are not three different features. They are one mechanism — "finish with this code and this value" — and the enclosing construct decides what to do about it.
5. return -code raises deliberately
Because they are one mechanism, return can produce any of the codes:
proc normal {} { return "a value" }
proc raises {} { return -code error "raised via return" }
puts [normal]
if {[catch {raises} msg]} {
puts "caught: $msg"
}a value
caught: raised via returnreturn -code error "msg" is equivalent to error "msg" from the caller's point of view. You met it in Chapter 10.5 §5; now you know why it exists — return is the general "finish with this code" command and error is the convenience spelling for the common case.
6. What raises an error
Not only your own error calls. Any command that cannot do its job:
foreach body {
{expr {1/0}}
{open /no/such/file r}
{lindex}
{set x $undefined}
{no_such_command}
} {
catch $body msg
puts [format " %-24s -> %s" $body $msg]
} expr {1/0} -> divide by zero
open /no/such/file r -> couldn't open "/no/such/file": no such file or directory
lindex -> wrong # args: should be "lindex list ?index...?"
set x $undefined -> can't read "undefined": no such variable
no_such_command -> invalid command name "no_such_command"All five are code 1 and all five unwind the same way. There is nothing special about an error you raised yourself.
7. Errors carry more than a message
Tcl records two extra pieces of information whenever an error propagates:
catch {expr {1/0}} msg
puts "message : $msg"
puts "errorCode : $::errorCode"
catch {open /no/such/file r} msg
puts "errorCode : $::errorCode"message : divide by zero
errorCode : ARITH DIVZERO {divide by zero}
errorCode : POSIX ENOENT {no such file or directory}errorCode is a machine-readable list — a class, a specific code and a message — which lets a script distinguish "file not found" from "permission denied" without matching on English text. And errorInfo holds the stack trace, which is Chapter 14.6's subject.
8. The model in one paragraph
A command finishes with a code and a value. Code 0 means the value is a result; any other code means the value is something else — an error message, a returned value, nothing at all — and the code keeps unwinding outward until a construct that understands it takes over. Loops understand 3 and 4. Procedures understand 2. catch understands all of them. If nothing does, the interpreter stops and prints the message.
9. Exercises
Exercise 1. What completion code is normal completion, and what code is an error?
Exercise 2. What happens to a command's error if nothing catches it?
Exercise 3. Name the five completion codes and what produces each.
Exercise 4. Why does return from inside two nested loops leave both of them?
Exercise 5. What is return -code error "msg" equivalent to?
Exercise 6. Name two things Tcl records about an error besides the message.
Answers
1. Normal completion is code 0; an error is code 1.
2. It unwinds the call stack frame by frame — out of the procedure, out of its caller, and so on — until something handles it. If nothing does, the interpreter stops and prints the message.
3. 0 normal completion, 1 error, 2 return, 3 break, 4 continue.
4. Because return produces completion code 2, which loops do not handle — only a procedure boundary does. So it unwinds through both loops and ends the procedure.
5. error "msg". From the caller's point of view they are indistinguishable; error is the convenience spelling and return -code is the general form.
6. errorCode, a machine-readable list such as POSIX ENOENT {no such file or directory}, and errorInfo, the stack trace.
10. Remember
- Every command finishes with a completion code. 0 is normal; anything else is not.
- An error is code 1. It unwinds the call stack until something handles it, or the script stops.
- The five codes are 0 ok, 1 error, 2 return, 3 break, 4 continue — one mechanism, not four features.
- That is why
returnescapes nested loops andbreakdoes not: loops handle 3 and 4, procedures handle 2. catchreports the code instead of letting it propagate.return -code error "msg"is equivalent toerror "msg".- Tcl's own failures are ordinary errors — nothing distinguishes them from yours.
errorCodeis machine-readable anderrorInfoholds the stack trace.
Where this fits
Part of the Tcl curriculum.
