Updated 2014-05-27 18:07:57 by st3ve

Do you need try/catch/finally?

Use the built-in try command, available since Tcl 8.6.

PURPOSE: Demonstrate a Tcl implementation of a [try ... finally ...] construct.

Original post: KBK (8 November 2000)

—Obsolete by the try command in Tcl 8.6—

Note: There is a TIP, including source code, to add "try...catch...finally" to the Tcl core, at: http://www.tcl.tk/cgi-bin/tct/tip/89

Note: the trycatch 2.0 package, available at http://www.wjduquette.com/tcl, includes a try ... finally ... construct, as well as other exception handling mechanisms. WHD (10 February 2001)

Note: See new TIP proposal for Try-Catch Exception Handling for try...catch with Tcl 8.5 error options support

Often in programming, it is essential that some action be taken whenever control leaves a given region of code for whatever reason. For instance, a piece of code may be using some system resource and wish to free it when it terminates, whether normally or abnormally. Several languages other than Tcl provide this capability naturally; Common Lisp has it in the form of (unwind-protect), and Java has it as try ... finally ....

One of the beauties of Tcl is that it is possible for advanced users to add new control structures by judicious use of [uplevel]. The following code implements a Tcl version of [try ... finally ...].

A simple example of how to use it:
 set f [open [lindex $argv 0] r]
 try {
     set i 0
     while { [gets $f line] >= 0 } {
         incr i
         puts $line 
         if { [string equal $line BADSTUFF] } {
             error "bad stuff encountered in [lindex $argv 0], line $i"
         }
     }
 } finally {
     close $f
 }

 #----------------------------------------------------------------------
 #
 # try --
 #
 #       Execute a Tcl script with a mandatory cleanup.
 #
 # Usage:
 #       try script1 finally script2
 #
 # Parameters:
 #       script1 -- Script to execute
 #       finally -- The literal keyword, "finally".
 #       script2 -- Script to execute after script2
 #
 # Results:
 #       See below.
 #
 # Side effects:
 #       Whatever 'script1' and 'script2' do.
 #
 # The [try] command evaluates the script, 'script1'.  It saves the
 # result of evaluating the script temporarily, and then evaluates
 # 'script2'.  If 'script2' returns normally, the result of the
 # 'try' is the result of evaluating 'script1', which may be
 # a value, an error, or a return, continue, or break.  If 'script2'
 # returns an error, or if it breaks, continues, or returns, the
 # action of 'script2' overrides that of 'script1'; the result
 # of the [try] is to return the error, break, continue, or return.
 #
 # Bugs:
 #       [return -code] within either script cannot be implemented.
 #       For this reason, [try] should not be used around scripts
 #       that implement control structures.
 #
 # Example:
 #    The following script:
 #
 #       set f [open $fileName r]
 #       try {
 #            while { [gets $f line] >= 0 } {
 #                processOneLine $line
 #            }
 #       } finally {
 #            close $f
 #       }
 #
 #    has the effect of ensuring that the file is closed, irrespective
 #    of what processOneLine does.  (If [close] returns an error, that
 #    error is returned in preference to any error from the 'try'
 #    block.)
 #
 #----------------------------------------------------------------------
 
 proc try { script1 finally script2 } {
     if { [string compare $finally {finally}] } {
         append message \
             "syntax error: should be \"" [lindex [info level 0] 0] \
             " script1 finally script2\""
         return -code error $message
     }
     set status1 [catch {
         uplevel 1 $script1
     } result1]
     if { $status1 == 1 } {
         set info1 $::errorInfo
         set code1 $::errorCode
     }
     set status2 [catch {
         uplevel 1 $script2
     } result2]
     switch -exact -- $status2 {
         0 {                             # TCL_OK - 'finally' was ok
             switch -exact -- $status1 {
                 0 {                     # TCL_OK - 'try' was also ok
                     return $result1
                 }
                 1 {                     # TCL_ERROR - 'try' failed
                     return -code error \
                            -errorcode $code1 \
                            -errorinfo $info1 \
                            $result1 
                 }
                 2 {                     #  TCL_RETURN
                     return -code return $result1
                 }
                 3 {                     # TCL_BREAK
                     return -code break
                 }
                 4 {                     # TCL_CONTINUE
                     return -code continue
                 }
                 default {               # Another code
                     return -code $code $result1
                 }
             }
         }
         1 {                             # TCL_ERROR -- 'finally' failed
             set info2 $::errorInfo
             set code2 $::errorCode
             append info2 "\n    (\"finally\" block)"
             return -code error -errorcode $code2 -errorinfo $info2 \
                 $result2
         }
         2 {                             # TCL_RETURN
             # A 'return' in a 'finally' block overrides
             # any status from the 'try' ?
             
             return -code return $result2
         }
         3 {                             # TCL_BREAK
             # A 'break' in a 'finally' block overrides
             # any status from the 'try' ?
             
             return -code break
         }
         4 {                             # TCL_CONTINUE
             # A 'continue' in a 'finally' block overrides
             # any status from the 'try' ?
             
             return -code break
         }
         default {                       # Another code in 'finally'
             # Another code in a 'finally' block is returned
             # overriding any status from the 'try'
             
             return -code $code $result2
         }
     }
 }

Note that TclX has a [try_eval]. Also, TclExcept [1] is a nice pure-Tcl package focused on exceptions, and mkGeneric [2] implements rather Java-like exceptions. Combat also has a try, as do several other implementations written by various people.

Martin Lemburg wrote a try-catch-finally package 2 years ago, trying to implement the capabilities of the C++ try-catch-finally-construct. [3] [4] Man pages included, but not heavily tested. It works for my needs!

DKF Here's a version that leverages new features in Tcl 8.5:
 proc try {script args} {
    upvar 1 try___msg msg try___opts opts
    if {[llength $args]!=0 && [llength $args]!=2} {
       return -code error "wrong \# args: should be \"try script ?finally script?\""
    }
    if {[llength $args] == 2} {
       if {[lindex $args 0] ne "finally"} {
          return -code error "mis-spelt \"finally\" keyword"
       }
    }
    set code [uplevel 1 [list catch $script try___msg try___opts]]
    if {[llength $args] == 2} {
       uplevel 1 [lindex $args 1]
    }
    if {$code} {
       dict incr opts -level 1
       return -options $opts $msg
    }
    return $msg
 }

TWAPI also includes a try-onerror-finally command.

RLH Is this going into 8.5?

NEM 2 Nov 2006: I'd guess that is extremely unlikely at this point. There is a TIP #89 [5] that proposes this, but it needs some updating. Probably too late to get that done in time for 8.5 feature freeze now.

RLH Actually the vote says a vote is "pending". Whatever that means.

Lars H: It just means that there is neither a vote going on at the monent, nor has a vote been completed. It does not mean that a vote will happen Real Soon Now.

RLH I asked on the core list. It is not going into 8.5 but if I remind them it could happen in 8.6.

escargo 2 Nov 2006 - Take a look at http://docs.python.org/whatsnew/pep-341.html to see how Python 2.5 is addresses similar issues.

Googie 15 Dec 2006 - It would be nice to see Java-like "try-catch-finally" here, including all it's possible syntaxes (excluding catch or finally). Any volunteer? I wrote some by myself but it doesn't seem to work correctly.

DKF: It's difficult to make it work right without 8.5; TIP#90 adds features that help a lot. However, it is still hard to make full 'try-catch-finally' mostly because it is not at all clear what the 'catch' clause(s) should match against. In turn, the real cause of that is just how lax Tcl has been for years over the consistent generation of useful errorCode information...

LV I am uncertain I understand the idea here. Is it that even if code within the try clause exits, the finally code will execute? Obviously if the code within the try core dumps, the finally isn't going to occur, no matter what code is written. So that seems to be the only situation that makes sense.

Is it safe to assume that try/finally's can be nested (how deeply) and occur from the most recently invoked to the least recently invoked during exit?

DKF: exit isn't implemented via exception codes, so in that case there will be no finally clause invokation.

LV Okay then, I'm really confused. How is try...finally any different than just coding without the try...finally?

DKF: Consider this:
  set f [open $file]
  try {
     while {[gets $f l] >= 0} {
        # ... do some processing
     }
  } finally {
     close $f
  }

This differs from just a normal sequence of commands when the stuff in the while throws an exception. In that case, the code will still close the channel. Doing the same in conventional Tcl requires catch and some fairly mucky code; that muckyness is what is wrapped in the "try ... finally ..." construct.

NEM 15 Dec 2006: While a try/finally construct is useful for dealing with errors, it's usually a good idea to wrap up its usage in higher-level constructs, rather than using it directly in application code. For instance, we can emulate C#'s using construct which does the same as DKF's example above:
 using f [open $file] {
     while {[gets $f l] >= 0} {
         # ... do some processing
     }
 }

Here all the exception handling and cleanup is hidden away in the using control structure. To take a bigger example, say we want to iterate through the articles in a newsgroup using the nntp package. We could do this using try/finally directly:
 set con [nntp::nntp $server $port]
 lassign [$con group comp.lang.tcl] num first last group
 # Limit to max 20 posts
 if {$last - $first > 20} { set first [expr {$last - 20}] }
 try {
    for {set id $first} {$id <= $last} {incr id} {
        try {
            set head [$con head $id]
            set body [$con body $id]
            puts [join $msg \n]
            puts "--------------------------------"
            puts [join $body \n]
            puts "================================"
        } ;# ignore errors in fetching individual articles
    }
 } finally { $con quit }

Or, we could again wrap this up in a higher-level control structure specialised to this task:
 foreachNewMsg {head body} [nntp::nntp $server $port] comp.lang.tcl {
     puts [join $head \n]
     puts "--------------------------------"
     puts [join $body \n]            
     puts "================================"
 }

(I'll put the full code for this construct on New Control Structures to save space here). This makes application code cleaner as it separates the code which deals with the network from the application logic. This means that we only have to deal with application-specific errors in the application-specific code, and network-specific errors in the network code, and we don't have to duplicate similar error-handling code every time we perform the same operation (see a higher-level channel API for another example). Of course, try/finally is useful in implementing such a construct, but I want to emphasise that having try/finally is only part of a solution to good exception handling. The greater half is in designing clean abstraction boundaries.

tcl_exceptions : A TCL package to add more sophisticated (Python-inspired) exception handling.
# Example usage:
# try {
#     Code that may need cleanup
#     and/or
#     Code with potential errors
# } catch -ex ExceptionType {
#     ExceptionType handling code...
# } catch -gl {Index*Error} {
#     Code to handle e.g., IndexOutOfRangeError, IndexTypeError...
# } catch -re {Bad(Arg|Data)Error} {
#     Code to handle BadArgError or BadDataError...
# } catch * {
#     Code to catch anything not handled by the previous catch blocks...
# } finally {
#     Cleanup code that always executes
# }