Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc f {} {
    error "bad input"
}

f
puts "this line never runs"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc inner {} { error "deep failure" }
proc middle {} { inner ; puts "middle continues" }
proc outer {} { middle ; puts "outer continues" }

outer
puts "top level continues"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Two parallel left-to-right flows. A command that succeeds completes with code zero, a value comes back, and the caller continues with it. A command that fails raises with code one, which unwinds the call stack until the nearest catch handles it, or the script stops.It succeedscompletes normallyCode 0a value comes backCaller continuesusing that valueIt failsraises an errorCode 1unwinds the stackCaught, or stopsthe nearest catch wins12
Figure 1 — the two ways a command can finish. Normal completion returns code zero along with a value, and the caller carries on using it. An error is completion code one: instead of handing back a value it unwinds the call stack, frame by frame, until the nearest enclosing catch handles it — and if nothing does, the script stops.

3. catch stops the unwinding

catch runs a script and reports its completion code instead of letting it propagate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
caught: deep failure
the script continues

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

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 ''
CodeMeansProduced by
0normal completionanything that finishes
1errorerror, or any failing command
2returnreturn
3breakbreak
4continuecontinue

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc normal {} { return "a value" }
proc raises {} { return -code error "raised via return" }

puts [normal]

if {[catch {raises} msg]} {
    puts "caught: $msg"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a value
caught: raised via return

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
catch {expr {1/0}} msg
puts "message   : $msg"
puts "errorCode : $::errorCode"

catch {open /no/such/file r} msg
puts "errorCode : $::errorCode"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 return escapes nested loops and break does not: loops handle 3 and 4, procedures handle 2.
  • catch reports the code instead of letting it propagate.
  • return -code error "msg" is equivalent to error "msg".
  • Tcl's own failures are ordinary errors — nothing distinguishes them from yours.
  • errorCode is machine-readable and errorInfo holds the stack trace.

Where this fits

Part of the Tcl curriculum.