TIP #329 Version 1.3: Try/Catch/Finally syntax

This is not necessarily the current version of this TIP.


TIP:329
Title:Try/Catch/Finally syntax
Version:$Revision: 1.3 $
Author:Trevor Davel <twylite at crypt dot co dot za>
State:Draft
Type:Project
Tcl-Version:8.6
Vote:Pending
Created:Monday, 22 September 2008
Discussions To:http://wiki.tcl.tk/21608
Obsoletes:TIP #89

Abstract

This TIP proposes the addition of new core commands to improve the exception handling mechanism. It supercedes TIP #89 by providing support for the error options dictionary introduced in Tcl 8.5 by TIP #90.

Rationale

See TIP #89 for general rationale for enhancing exception handling.

The try syntax presented here is not intended to replace catch, but to simplify the expression of existing exception/error handling techniques, leading to greater code clarity and less error-prone workarounds for finally blocks. There is no deficiency in the functionality of Tcl's exception handling mechanisms - what is lacking is a more readable syntax and a standard for behaviour across packages for the common case of catching a subset errors or exceptions that are thrown from within a particular block of code.

In Tcl 8.4 exceptions could be caught using catch, and exception information was available via the catch return value and resultvar. If the return value was TCL_ERROR (1) then the globals ::errorCode and ::errorInfo would be set according to the exception raised. TIP #89 was written to work with this model, such that a catch handler (in a try...catch) would be able to capture the resultvar, errorCode and errorInfo.

Tcl 8.5 implements TIP #90 which extends catch to allow an additional dictionary of options (error information) to be captured. These options supercede the ::errorInfo and ::errorCode globals (though those are still supported for backward compatibility). It is therefore logical to extend/correct the syntax of TIP #89 to support the options dictionary in preference to the older mechanism for capturing exception information.

Benefits of adding this functionality to the core:

Specification

try body ?as {resultVarName ?optionsVarName?}? ?handler ...? ?finally body?

throw type message

The try body is evaluated in the caller's scope. The handlers (on and trap) are searched in order of declaration until a matching one is found, and the associated body is executed. If no matching handler is found then try returns the result of the try body (exceptions will propagate up the stack as usual); otherwise try returns the result of the handler body.

The finally body (if present) will be executed last, and is always executed whatever the results of the try and handler bodies (excepting resource exhaustion or cancellation). If the finally body returns an exceptional code then this will become the result of try, otherwise the result of the finally body is ignored.

Only one handler body (that of the first matching handler) will be executed. If the handler body is the literal string "-" then the body for the subsequent handler will be used instead. It is an error for the last handler's body to be a literal "-".

If the as clause is present the results of executing the try body will be assigned into the given variables in the caller's scope, as with [catch].

Since the trap handlers in the try control structure are filtered based on the exception's -errorcode, it makes sense to have a command that will encourage the use of error codes when throwing an exception. throw is merely a reordering of the arguments of the error command. type SHOULD be constructed as a list to maintain compatibility with ::errorcode, but it is treated as a string by trap (see below).

Handlers

on code body

The on handler allows exact matching against the exceptional return code (the integer value that would be returned by catch). code may be given as an integer or one of the magic keywords ok (0), error (1), return (2), break (3), continue (4).

trap pattern body

The trap handler allows glob-style pattern matching against the -errorcode when the exceptional return code is TCL_ERROR (1). For this match the -errorcode is treated as a string.

Notes & clarifications:

Examples

Simple example of try/handler/finally logic in Tcl using currently available syntax:

 proc read_hex_file {fname} {
    set f [open $fname "r"]
    set data {}
    set code [catch {
       while { [gets $f line] >= 0 } {
          append data [binary format H* $line]
       }
    } em opts]
    if { $code != 0 } {
       dict set opts -code 1
       set em "Could not process file '$fname': $em"
    }
    close $f
    return -options $opts $em
 }

And the same example rewritten to use [try]:

 proc read_hex_file {fname} {
    set f [open $fname "r"]
    set data {}
    try  {
       while { [gets $f line] >= 0 } {
         append data [binary format H* $line]
       }
    } as {em} trap * {
       error "Could not process file '$fname': $em"
    } finally {
       close $f
    }
 }

This illustrates how the intent of the code is more clearly expressed by [try], but does not demonstrate the use of multiple catch blocks.

References

Rejected Alternatives

Various alternatives are discussed on the wiki [3] along with reasons for their rejection.

Future Extensions

No specific future exceptions are planned, but try could be extended by adding new handler keywords and/or introducing new varnames to the as list.

Reference Implementation

 namespace eval ::control {
 
   # These are not local, since this allows us to [uplevel] a [catch] rather than
   # [catch] the [uplevel]ing of something, resulting in a cleaner -errorinfo:
   variable em {}
   variable opts {}
 
   set ON_CODES { ok 0 error 1 return 2 break 3 continue 4 }
 
 }
 
 proc ::control::throw {type message} {
   return -code error -errorcode $type -errorinfo $message -level 2 $message
 }
 
 # For future reference: rethrow can be implemented by adding a "-rethrow"
 # key to the return options dict
 # proc ::control::rethrow {{type {}} {message {}}} {
 #   return -code error -errorcode $type -rethrow 1 $message
 # }
 
 proc ::control::try {args} {
 
   variable ON_CODES
 
   # Check parameters
     set try_block [lindex $args 0]
     set handlers {}
     set finally {}
     set as_result {}
     set as_options {}
     set i 1
 
     # Optional "as {resultVarName ?optionsVarName?}"
     if { [lindex $args $i] eq "as" } {
       lassign [lindex $args $i+1] as_result as_options
       incr i 2
     }
     
     # Handlers & finally
     while { $i < [llength $args] } {
       switch -- [lindex $args $i] {
         "on" {
           # on code body
           # translate code to integer
           if { [scan [lindex $args $i+1] %d%c code dummy] != 1 } {
             # not a number - try the magic keywords
             if { [dict exists $ON_CODES [lindex $args $i+1]] } {
               set code [dict get $ON_CODES [lindex $args $i+1]]
             } else {
               # otherwise its an error
               break
             }            
           }
           # otherwise store the handler for later
           lappend handlers "${code},*" [lindex $args $i+2] 
           incr i 3
         }
         "trap" {
           # trap pattern body
           # store the handler for later
           lappend handlers "1,[lindex $args $i+1]" [lindex $args $i+2]
           incr i 3
         }
         "finally" {
           # finally body (and no further handlers)
           set finally [lindex $args $i+1]
           incr i 2
           break
         }
         default {
           # unrecognised handler keyword
           break
         }
       }
     }
 
     # If we broke out before the last arg (or need more args) then there is a 
     # parameter problem
     # If the last handler body is a "-" then reject
     if { $i != [llength $args] || [lindex $handlers end] eq "-" } {
       error "wrong # args: should be \"try body ?as {resultVar ?optionsVar?}? ?on code body ...? ?trap pattern body ...? ?finally body?\""
     }
     
   # Execute the try_block, catching errors
     variable em
     variable opts
     set code [uplevel 1 [list ::catch $try_block \
       [namespace which -variable em] [namespace which -variable opts] ]]
       
   # Assign try body result to caller's variables
   if { $as_result ne {} } {
     upvar $as_result _as_em
     set _as_em $em
     if { $as_options ne {} } {
       upvar $as_options _as_opt
       set _as_opt $opts
     }
   }
     
   # Keep track of the original error message & options
     set _em $em
     set _opts $opts
 
   # Find and execute handler
     set errorcode {}
     if { [dict exists $_opts -errorcode] } {
       set errorcode [dict get $_opts -errorcode]
     }
     set exception "$code,$errorcode"
 
     set found false
     foreach {pattern body} $handlers {
       if { ! $found && ! [string match $pattern $exception] } continue
       set found true
       if { $body eq "-" } continue
       
       # Handler found - execute it
       set code [uplevel 1 [list ::catch $body \
         [namespace which -variable em] [namespace which -variable opts] ]]
             
       # Handler result replaces the original result (whether success or
       # failure); capture context of original exception for reference
       dict set opts -during $_opts
       set _em $em
       set _opts $opts
     
       # Handler has been executed - stop looking for more
       break
     }
     
     # No catch handler found -- error falls through to caller
     # OR catch handler executed -- result falls through to caller
 
   # If we have a finally block then execute it
     if { $finally ne {} } {
       set code [uplevel 1 [list ::catch $finally \
         [namespace which -variable em] [namespace which -variable opts] ]]
       
       # Finally result takes precedence except on success
       if { $code != 0 } {
         dict set opts -during $_opts
         set _em $em
         set _opts $opts
       }
       
       # Otherwise our result is not affected
     }
 
   # Propegate the error or the result of the executed catch body to the caller
 
     #FIXME -level 2 will hide the try...catch itself from errorInfo, but it
     #  breaks nested 'try { try ... catch } catch'
     dict incr _opts -level 1
 
     return -options $_opts $_em
 }
 
 interp alias {} ::try {} ::control::try
 interp alias {} ::throw {} ::control::throw

Thanks

Thanks in particular to DKF, NEM and JE for their feedback and suggestions on this TIP.

Copyright

This document has been placed in the public domain.


Powered by TclThis is not necessarily the current version of this TIP.

TIP AutoGenerator - written by Donal K. Fellows