This is not necessarily the current version of this TIP.
| TIP: | 329 |
| Title: | Try/Catch/Finally syntax |
| Version: | $Revision: 1.4 $ |
| 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 |
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.
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:
Bring to Tcl a construct commonly understood and widely used in other languages.
A standard for identifying categories/classes of errors, which will improve interoperability between packages.
A byte-coded implementation would be significantly faster than the Tcl implementation that is presented.
try body ?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 "-".
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).
Each handler is identified by a keyword. The fields following the keyword indicate what exceptions or errors are matched by the handler, the variables into which the result of the try will be assigned (in the caller's scope), and the body of the handler.
on code {?resultVar ?optionsVar?} 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 {?resultVar ?optionsVar?} body
The trap handler allows list prefix matching against the -errorcode when the exceptional return code is TCL_ERROR (1). The -errorcode is treated as a list (it is not an exception if the -errorcode cannot be interpreted as a list; the handler will simply not match).
Given a pattern and an errorcode, a list prefix match is successful if for every element in pattern there is a corresponding and identical element in errorcode. Trailing elements in errorcode are ignored.
Notes & clarifications:
Handlers are searched in order of declaration (left-to-right). One consequence of this search order is that an on error handler will supercede all subsequent trap handlers.
The result of the last executed body (other than the finally body) is the result of the try. Exceptions in any body (including the finally body) replace the existing exception and propagate.
Any unhandled exception propagates.
If any exception is replaced (by an exception in a handler body or in the finally body) then the new exception shall introduce into its options dict the field -during that contains the options dict of the exception that was replaced.
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]
}
} trap {POSIX} {} {
puts "POSIX-type error"
} on error {em} {
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.
Various alternatives are discussed on the wiki [3] along with reasons for their rejection.
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.
''To be updated following removal of the as clause''
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 in particular to DKF, NEM and JE for their feedback and suggestions on this TIP.
This document has been placed in the public domain.
This is not necessarily the current version of this TIP.