Welcome, guest | Sign In | My Account | Store | Cart

The Java try ... catch ... finally construct is really nice for those cases when you want to guarantee to always release a resource (particularly a file handle) even if the code generates an exception. It is also very useful to be able to use different types of error handling depending on the error that occurred. This code implements such a facility in pure Tcl.

Tcl, 94 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
namespace eval ::try {
   namespace export try
   variable bodyMatch {    \("uplevel" body line}; # Note: must be FOUR spaces
   variable usage {unknown keyword "%s" to try: should be "try body ?catch matcher body ...? ?finally body?"}

   # Some code that is factorised out.  It runs the given script in the context of our
   # caller's caller and, if that call generates an error, removes the extra junk inserted
   # into the error trace due to this.  All results are passed back by setting variables
   # in our caller's context...
   # The part argument gives the part of the error trace to insert into the error trace
   # in the case of an error occurring so as to indicate what the context of the error
   # w.r.t. the try command really is.
   proc helper {script part {eiv ei} {ecv ec} {codev code} {msgv msg}} {
      global errorInfo errorCode
      variable bodyMatch
      upvar 1 $eiv ei $ecv ec $codev code $msgv msg
      set code [catch [list uplevel 2 $script] msg]; # Note: unusual uplevel parameter
      set ec $errorCode
      set lines [split $errorInfo "\n"]
      if {$code == 1} {
         while {![regexp $bodyMatch [lindex $lines end]]} {
            set lines [lrange $lines 0 [expr {[llength $lines]-2}]]
         }
         regsub {"uplevel" body} [lindex $lines end] $part fixed
         set lines [lrange $lines 0 [expr {[llength $lines]-2}]]
         lappend lines $fixed
      }
      set ei [join $lines "\n"]
   }

   # The main command's implementation (see example for syntax.)
   proc try {body args} {

      # First, parse apart the args.  This is relatively straight-forward
      set hasFinally 0
      set catches {}
      for {set i 0} {$i<[llength $args]} {incr i} {
         set word [lindex $args $i]
         if {![string compare $word catch]} {
            if {$i+1 >= [llength $args]} {
               return -code error "missing matcher to catch"
            } elseif {$i+2 >= [llength $args]} {
               return -code error "missing body to catch"
            }
            lappend catches [lindex $args [incr i]]
            lappend catches [lindex $args [incr i]]
         } elseif {![string compare $word finally]} {
            if {$i+1 >= [llength $args]} {
               return -code error "missing body to finally"
            }
            set finally [lindex $args [incr i]]
            set hasFinally 1
         } else {
            variable usage
            return -code error [format $usage $word]
         }
      }

      # Now evaluate the body.  This updates variables "code", "ei", "ec" and "msg"
      # with the result code, the error trace, the error detail and the returned value
      # respectively...
      helper $body "try body"

      # Handle errors, if there were any.  Note that if an error does occur and gets
      # handled, the details of what the error was are lost as part of the processing.
      # Doing something more sophisticated is left as an exercise to the reader...
      if {$code == 1} {
         foreach {matcher handler} $catches {
            if {[string match $matcher $ec]} {
               helper $handler "catch \"$matcher\" body"
               break
            }
         }
      }

      # Do the finally clause.  This only wipes out the result information if the clause
      # causes an error.  Otherwise, the clause has no effect.  In particular, you cannot
      # (successfully) use return, break or continue in a finally clause.
      if {$hasFinally} {
         helper $finally "finally clause" a b c d
         if {$c} {
            set ei $a
            set ec $b
            set code $c
            set msg $d
         }
      }

      # Now we can return.  Phew!
      return -code $code -errorinfo $ei -errorcode $ec $msg
   }
}

namespace import ::try::try

It is pretty easy to use the above script. Here's a demonstration: try { set f [open /non/existant/file] } catch {POSIX ENOENT *} { puts "That file doesn't exist!" } catch {POSIX EACCES *} { puts "You may not open that file!" } finally { puts "Wibble..." }

Usage of this sort of thing is actually more important in Tcl than in Java, because a great many Tcl resources (particularly commands and channels) are not garbage collected, and so must be deleted explicitly, a process which can be easy to forget to do when an error happens. With this, you can make cleanup in all possible error cases a much simpler chore, and provide extra special handling for some errors.

The error handling of this code is based on the comparatively little-used ::errorCode variable. This makes it ideal for handling system problems, but you should note that it is all too easy to forget to create special error types for your own code, which converts this tool into something rather blunt. Remember to create those machine-readable errors; both [return] and [error] support them!

2 comments

andreas kupries 22 years, 6 months ago  # | flag

Candidate for "control" module in Tcllib. This code is a candidate for the "control" module in Tcllib.

Nir Levy 21 years, 7 months ago  # | flag

Funny... funny, i just started writing the very same thing last night :-)

anyways i recommend a different way to use the construct:

try {
    puts "try something"
} catch { something == $something_else } {
    puts "catch will eval any expr and proccess it's body only if tru"
} catch { default } {
    puts "the 'default' keyword will catch anything (like 1==1)"
} finally {
   cleanup code
}

there is also a need to decide on a break/continue semantics in catch clauses...

any thoughts? /NL

Created by Donal Fellows on Tue, 11 Sep 2001 (MIT)
Tcl recipes (162)
Donal Fellows's recipes (6)

Required Modules

  • (none specified)

Other Information and Tasks