Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {[catch {expr {1/0}} result]} {
    puts "failed: $result"
} else {
    puts "ok: $result"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
failed: divide by zero

catch 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach body {
    {expr {6 * 7}}
    {expr {1/0}}
} {
    set rc [catch $body result]
    puts [format "  %-14s rc=%d  result='%s'" $body $rc $result]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 skipped

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wide catch says   : can't read "path": no such variable
narrow catch says : couldn't open "missing.txt": no such file or directory

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
catch {expr {1/0}} result opts

puts "result : $result"
puts "options:"
foreach {k v} $opts {
    puts [format "  %-12s %s" $k [string map {"\n" " / "} $v]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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   1

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach {label body} {
    ok       {set x 1}
    error    {error "boom"}
    return   {return "early"}
    break    {break}
    continue {continue}
} {
    puts "  [format %-9s $label] -> [catch $body r]"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ok        -> 0
  error     -> 1
  return    -> 2
  break     -> 3
  continue  -> 4

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
good : the good one
bad  : couldn't open "missing.txt": no such file or directory

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  a.txt          6 bytes
  missing.txt    unavailable

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open $path r]
set code [catch { ...work... } result]
close $fp
if {$code} { return -code error $result }
return $result

10. 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 catch swallows your own typos.
  • The options dictionary carries -code, -errorcode, -errorinfo and -errorline.
  • catch also reports codes 2, 3 and 4 for an escaped return, break or continue.
  • Cleanup pattern: catch the work, clean up, then return -code error to re-raise — cleanup without suppression.

Where this fits

Part of the Tcl curriculum.