Tcl Part I · Module 14 · Errors and Debugging
catch: Handling an Error Without Stopping
catch runs a script and reports how it finished instead of letting a failure unwind past it. You have been using it since Module 7 — around open, around dict get, around procedure bodies — and this chapter settles the details: what it returns, where the message goes, and the one argument you have not met. It also makes the case against using it too freely, because a catch wrapped around more than you meant will swallow your own typos as readily as the failure you were prepared for, and a script that cannot fail is not the same as a script that works.
Foundation11 min readTclPart IErrorscatchRobustness
Part I · Module 14 · Chapter 14.3 · catch: Handling an Error Without Stopping
1. The shape
if {[catch {expr {1/0}} result]} {
puts "failed: $result"
} else {
puts "ok: $result"
}failed: divide by zerocatch script ?resultVar? runs the script and returns its completion code — 0 for success, 1 for an error. The script's value, or the error message, goes into the variable.
Because it returns 0 or 1, it reads naturally as a condition. Note the sense: if {[catch ...]} is "if it failed", which is the opposite of how most code's conditions read and worth a second's attention each time you write one.
2. The result variable holds two different things
foreach body {
{expr {6 * 7}}
{expr {1/0}}
} {
set rc [catch $body result]
puts [format " %-14s rc=%d result='%s'" $body $rc $result]
} expr {6 * 7} rc=0 result='42'
expr {1/0} rc=1 result='divide by zero'On success it is the value; on failure it is the message. That dual use is why catch {open $path r} fp reads oddly the first time — fp is a channel handle when it worked and an error message when it did not, which is exactly what Chapter 12.8 §3 relied on.
3. The skip-and-continue pattern
The commonest real use — process what you can, report what you cannot:
proc read_first_line {path} {
set fp [open $path r]
set line [gets $fp]
close $fp
return $line
}
# one real file; the others do not exist
set fp [open good.txt w] ; puts $fp "the good one" ; close $fp
set ok 0
set failed 0
foreach path {good.txt missing.txt also-missing.txt} {
if {[catch {read_first_line $path} result]} {
puts " skipping $path: $result"
incr failed
continue
}
puts " $path: $result"
incr ok
}
puts "$ok read, $failed skipped" good.txt: the good one
skipping missing.txt: couldn't open "missing.txt": no such file or directory
skipping also-missing.txt: couldn't open "also-missing.txt": no such file or directory
1 read, 2 skippedThree things make this the shape to copy. The catch wraps the call that can fail and nothing else. The failure is reported rather than silently swallowed — a skipped input the operator never hears about is as bad as a crash. And the loop keeps a count of both outcomes, so the summary line says what actually happened.
4. catch the smallest thing that can fail
A catch around a large block tolerates failures you never considered — including your own mistakes:
proc process {path} {
return "processed $path"
}
# too wide: a typo inside is indistinguishable from a missing file
if {[catch {
set fp [open good.txt r]
set data [gets $fp]
close $fp
procezz $path ;# typo, and an undefined variable
} result]} {
puts "wide catch says : $result"
}
# narrow: only the thing that can legitimately fail
if {[catch {open missing.txt r} fp]} {
puts "narrow catch says : $fp"
}wide catch says : can't read "path": no such variable
narrow catch says : couldn't open "missing.txt": no such file or directoryThe wide catch reported can't read "path" — an undefined variable in code that was never meant to be tolerant. It reached that line before the misspelled procezz even ran, and either way the mistake was turned into a handled condition. Had that branch simply logged and continued, the script would run to completion having silently done nothing.
5. The third argument: the options dictionary
catch script ?resultVar? ?optionsVar? gives you everything Tcl knows about the failure:
catch {expr {1/0}} result opts
puts "result : $result"
puts "options:"
foreach {k v} $opts {
puts [format " %-12s %s" $k [string map {"\n" " / "} $v]]
}result : divide by zero
options:
-code 1
-level 0
-errorcode ARITH DIVZERO {divide by zero}
-errorinfo divide by zero / invoked from within / "expr {1/0}"
-errorline 1That is the structured form of the information Chapter 14.1 §7 introduced: the code, the machine-readable errorcode, and the stack trace. Chapter 14.6 uses it to re-raise an error without losing its history.
6. catch returns more than 0 and 1
Because completion codes are one mechanism (Chapter 14.1 §4), catch can report a return, break or continue that escaped:
foreach {label body} {
ok {set x 1}
error {error "boom"}
return {return "early"}
break {break}
continue {continue}
} {
puts " [format %-9s $label] -> [catch $body r]"
} ok -> 0
error -> 1
return -> 2
break -> 3
continue -> 4Writing if {[catch ...]} treats every non-zero code as an error, which is usually fine and occasionally wrong. Chapter 14.4 is about when it is wrong.
7. Cleaning up on both paths
The pattern this corpus has used since Chapter 12.1 §6, now explained:
proc read_all {path} {
set fp [open $path r]
set code [catch {read -nonewline $fp} result]
close $fp
if {$code} { return -code error $result }
return $result
}
puts "good : [read_all good.txt]"
if {[catch {read_all missing.txt} m]} { puts "bad : $m" }good : the good one
bad : couldn't open "missing.txt": no such file or directoryThe close runs whether the read succeeded or failed, and the failure is still reported rather than swallowed — return -code error re-raises it. That last step is what distinguishes cleanup from suppression.
Chapter 14.5 shows the try/finally spelling for interpreters that have it, and why this remains the portable form.
8. A worked example
proc safe_size {path} {
if {[catch {file size $path} result]} {
return -1
}
return $result
}
set fp [open a.txt w] ; puts $fp "hello" ; close $fp
foreach p {a.txt missing.txt} {
set s [safe_size $p]
if {$s < 0} {
puts [format " %-14s unavailable" $p]
} else {
puts [format " %-14s %d bytes" $p $s]
}
} a.txt 6 bytes
missing.txt unavailableThe catch wraps exactly one command, the sentinel is distinguishable from any real size, and the caller decides what "unavailable" means.
9. Exercises
Exercise 1. What does catch return, and where does the message go?
Exercise 2. Why does if {[catch {...}]} read as "if it failed"?
Exercise 3. What does the result variable hold on success?
Exercise 4. Why is a catch around twenty lines usually wrong?
Exercise 5. What is in the options dictionary from catch's third argument?
Exercise 6. Write the shape that closes a channel on both the success and failure paths and still reports the failure.
Answers
1. The completion code — 0 for success, 1 for an error, and 2, 3 or 4 for an escaped return, break or continue. The script's value, or the error message, goes into the optional result variable.
2. Because catch returns 0 when the script succeeded, and 0 is false. So a true condition means a non-zero code, which means something other than normal completion.
3. The script's result — the value it produced. The same variable holds the error message when the code is non-zero, which is why it is only meaningful once you have checked the code.
4. Because it treats every possible failure in those twenty lines as the same handled condition, including typos and wrong-argument errors you never anticipated. The result is a script that never crashes and quietly does nothing.
5. -code, -level, -errorcode, -errorinfo and -errorline — the completion code, the machine-readable classification, the stack trace and the line number.
6.
set fp [open $path r]
set code [catch { ...work... } result]
close $fp
if {$code} { return -code error $result }
return $result10. Remember
catch script ?resultVar? ?optionsVar?returns the completion code and puts the value or message in the variable.if {[catch ...]}means "if it failed" — 0 is success and therefore false.- The result variable holds the value on success and the message on failure, so it is only meaningful after checking the code.
- Catch the smallest thing that can fail. A wide
catchswallows your own typos. - The options dictionary carries
-code,-errorcode,-errorinfoand-errorline. catchalso reports codes 2, 3 and 4 for an escapedreturn,breakorcontinue.- Cleanup pattern:
catchthe work, clean up, thenreturn -code errorto re-raise — cleanup without suppression.
Where this fits
Part of the Tcl curriculum.
