TIP 290: Registration of Custom Error Handler Scripts

Login
Bounty program for improvements to Tcl and certain Tcl packages.
Tcl 2017 Conference, Houston/TX, US, Oct 16-20
Send your abstracts to tclconference@googlegroups.com
by Aug 21.
Author:         Eckhard Lehmann <ecky-l@web.de>
Author:         Larry W. Virden <lvirden@yahoo.com>
State:          Draft
Type:           Project
Vote:           Pending
Created:        29-Oct-2006
Post-History:   
Keywords:       Tcl,error,trap
Tcl-Version:    8.7

Abstract

This TIP proposes the possibility to register a custom command as error and exception handler.

Rationale

Errors are thrown in the Tcl interpreter through the error command or from a C extension that returns TCL_ERROR. When an error is thrown, the global errorInfo variable is filled with rudimentary stacktrace information and the error message itself. The global errorCode variable can contain an error code if this is provided by the command that has thrown the error.

Errors can be caught with the catch command. In this case, the errorInfo variable is still filled with the information mentioned above, but the error is not presented to the interpreter. If the error is not caught, it is presented to the interpreter and the execution of the current code is aborted immediately

The information in errorInfo is, in some simple cases, useful for reproducing and tracking down the error source and fixing the problem. In more complicated cases however, errorInfo does not include enough information to successfully reproduce the error - information about the application's state is missing.

In other languages such as LISP, this problem is addressed by stopping the execution at the position where the error was thrown (preserving the current callframe) and presenting the developer with a console that enables him to introspect the running program. Although Tcl has very good introspection capabilities, it is not possible to use them in an error case, because the execution just aborts and the stacktrace is unwound at once. For errors generated with the error command, it is possible to overwrite this command and provide more advanced functionality, but this is not possible if errors are generated in C code by return TCL_ERROR.

The proposed implementation addresses this problem by a custom error command that is executed whenever an error occurs in the execution of Tcl code. This opens a range of implementation possibilities for error handling, for instance:

* registering a _breakpoint_ command that stops execution at the error
  position and opens a console for introspection

* registering a more advanced \(Tk\) debugger that opens on error for
  introspection.

* registering a command that captures the state of each call-frame up to
  the one where the error was thrown and writes that state to a file. This
  file can later be debugged \(with an appropriate tool\).

Alternatives

  1. Overwriting of the error command As stated abovem, this works only for errors generated from Tcl code, whereas it does not work for errors from C extensions.

  2. Leavestep execution traces In Tcl >=8.4 it is possible to register execution traces on leavestep to commands. To implement part of the proposed functionality it would be possible to add a leavestep trace procedure, which - after each command - checks for the return code and acts accordingly. The problem with this approach is, that the procedure is called after each command, independent from whether it returned an error or not. This can slow down the program execution more or less significantly. Furthermore, it is not easy if at all possible to capture occurences of catch by this way, so it is complicated or impossible to react on caught errors.

It might be possible to get part or most of the proposed functionality by trace add execution leavestep or overwriting of error. But it is the responsibility of the core language (even more when it is a dynamic language) to provide customizable and advanced error handling. The TIP proposes in this direction.

Specification

The implementation consists of two parts: a registration command for the custom command and a place where the handler is called. For this to work, there are some minor changes necessary to the Tcl execution engine and to the Interp structure. For running the handler on caught and or uncaught errors (depending on how the user wants to have it) it is necessary to capture the current level of "catch"es that occure during execution.

The registration command is responsible for:

* register the command for caught and/or uncaught exceptions

* retrieve the currently registered command

* change the execution details \(caught and/or uncaught errors\) and the command

* unregister the command and thus get back to the current behaviour in error cases

Since the functionality is very similar to the family of trace commands, the proposed registration command is an extension to trace:

trace set exception ?-caught? ?-uncaught? ?command?: Registration and modification

The arguments -caught and/or -uncaught to trace set exception modify the run conditions for the currently registered handler (run on caught/uncaught errors). With the command argument, this is set as the new handler. The return code and result of the command that was executing is appended to the registered command. So, the real call is: command code result.

trace info exception: Information about the current handler

This command returns a list with the elements {-caught y/n -uncaught y/n script}, where caught and uncaught flags are specified and script is the currently registered handler.

trace unset exception: Remove registerred handler

Any previously set error handler is unregistered.

The command that is registered will quell the error if it returns normal (return code 0). If the script returns abnormal, it's return code is returned to the interpreter. Errors inside the handler are not trapped by the script again, rather they are presented to the interpreter as usual - otherwise this would result in an endless loop.

The changes in the execution engine should be done so that:

* existing functionality is not disturbed

* the call frame is preserved after the error occured - thus the custom command is run in the same level as where the error was thrown

* the _::errorInfo_ and _::errorCode_ variables are updated to contain the error information **that is available in the current callframe**. This information must be updated before the custom command is run, so that it is accessible from there.

The innermost function that is called on Tcl code execution is TclEvalObjvInternal(). It is called from others to execute a command and returns the code that the executed command returned. It's the best place to trigger the error handler execution, but whether errors are catched (catchLevel) must be present at this time. Therefore, this level is stored in the current Interp* from within the callers of TclEvalObjvInternal(). The catch level can be determined either from TclExecuteByteCode() or from Tcl_CatchObjCmd() directly.

The errorInfo and errorCode variables are set directly before the handler is run. This ensures that they are updated properly. Eventually registerred traces on this variable are handled as usual, before the custom error command is executed.

Reference Implementation

A reference implementation is available from sourceforge as a patch against Tcl 8.5a5 http://sourceforge.net/support/tracker.php?aid=1587317 .

Usage Example

Here is a sample procedure that can be used to stop execution on error (return code 1) and introspect the program using stdin/stdout. Other return codes are returned together with the previous result. The procedure was mainly implemented by Neil Madden as debug-repl and is available (with a short discussion on the topic) on http://lambda-the-ultimate.org/node/1544#comment-18446 :

package provide debug 1.0

proc up {} {
    uplevel 2 {
        breakpoint
    }
}

proc down {} {
    return -code continue
}

proc breakpoint {code result} {
    if {$code > 1} {
        return -code $code $result
    }
    set cmd ""
    set level [expr {[info level]-1}]
    set prompt "Debug ($level) % "
    while {1} {
        puts -nonewline $prompt
        flush stdout
        gets stdin line
        append cmd $line\n
        if {[info complete $cmd]} {
            set code [catch {uplevel #$level $cmd} result]
            if {$code == 0 && [string length $result]} {
                puts stdout $result
            } elseif {$code == 3} {
                break
            } elseif {$code == 4} {
                # continue
                return
            } else {
                puts stderr $result
            }
            set prompt "Debug ($level) % "
            set cmd ""
        } else {
            set prompt "    "
        }
    }
}

To use it for uncaught errors, the breakpoint procedure can be registered as error command:

package re debug
trace set exception -uncaught breakpoint

When an error raises, breakpoint is called in the current callframe and Tcl introspection commands like info vars etc. can be used to get information about the program state.

Copyright

This document has been placed in the public domain.

History