Exception handling in promises

Published , updated

In a prior post I had illustrated the use of the Tcl promise package for asynchronous computing with some examples. There we had ignored the possibility of errors and exceptions and how they are handled in promise-based code. In this post, we build on the examples in that post to illustrate how promises greatly simplify handling of errors in async code.

Handling errors with promises

One of the most useful features of the promise abstraction is the ability to funnel errors and exceptions into a common error handling path. This point cannot be stressed enough. Writing async code is hard enough as it is and when you add in error handling and recovery into a event or callback based implementation, it quickly becomes (even more) unmanageable.

Consider an extension of the simple example in our previous post - given a list of integers, we want to generate the list of the corresponding Fibonacci numbers. That example did not deal with errors at all. We will now add a few simple checks and handle errors appropriately.

The sequential version

First, here is the sequential version of the modified code with error handling. First, some utility procedures,

proc display {numbers} {
    tk_messageBox -message "Requested Fibos are [join $numbers ,]"
}
proc display_error {error_message error_dictionary} {
    tk_messageBox -message "Error message: $error_message\nError code: [dict get \
        $error_dictionary -errorcode]" -icon error
}
proc check {n} {
    if {![string is integer -strict $n] ||
        $n < 1 || $n > 1000} {
        throw {FIBONACCI INVALIDARG} "Invalid argument $n: must be an integer in \
            the range 1:1000."
    }
}

and then the code itself

if {[catch {
    display [lmap n $numbers {
        check $n
        math::fibonacci $n
    }]
} error_message error_dictionary]} {
    display_error $error_message $error_dictionary
}

The async version

As before, we would like to rewrite this to run concurrently so as to not block our user interface while the computation is ongoing. We see how to do this with promises.

Before we do that though, we will take a small detour. In the previous post we made use of the ptask command to run the computation for each element of the list in a separate thread. This is wasteful at best and potentially disastrous in terms of system resources at worst. Moreover, each new thread has the overhead of creating a Tcl interpreter, loading the math package and so on. Fortunately, the Thread package implements a thread pool capability which resolves this issue. It permits a limited number of worker threads to be configured within a pool, and allows jobs to be queued which will be serviced by a free worker thread. By a happy coincidence, the promise package supports it with the pworker command.

Our promise-based async version therefore starts off creating a thread pool and initializing it by loading the required packages and defining the utility procedures.

package require Thread
set tpool [tpool::create -initcmd {
    package require math
    proc check {n} {
        if {![string is integer -strict $n] ||
            $n < 1 || $n > 1000} {
            throw {FIBONACCI INVALIDARG} "Invalid argument $n: must be an \
                integer in the range 1:1000."
        }
    }
}]
tpool::preserve $tpool

See the documentation for the Thread package for any questions with regards to the above.

We kick off the computation in almost identical fashion as in our previous post except for a small change related to the use of pworker command instead of ptask. If you are unsure about what the code below does, please see the prior post.

set computation [all [lmap n $numbers {
    pworker $tpool [lambda n {
        check $n
        math::fibonacci $n
    } $n]
}]]

Now we just use the done method to wait for the computation to complete and display the result.

$computation done display display_error

The first argument to the done method is the command prefix to invoke on fulfillment (successful completion) of the promise. An additional argument, the result of the computation, is appended to the command prefix before it is called. The second argument to done, which is optional, specifies a command prefix to be called when the promise is rejected. When this is called, it is passed two additional arguments - the error message and the Tcl error dictionary in the same format as returned by the catch command in case of errors.

Thus in our example, if all the worker threads complete sucessfully, the display command will be called and passed the list of computed Fibonacci numbers. In case one or more threads raise an error, the display_error command will be called and passed the error message and error dictionary from the thread.

As a point of comparison, here's the sequential and promise-based code listed together. First the synchronous version,

if {[catch {
    display [lmap n $numbers {
        check $n
        math::fibonacci $n
    }]
} error_message error_dictionary]} {
    display_error $error_message $error_dictionary
}

and the async version

set computation [all [lmap n $numbers {
    pworker $tpool [lambda n {
        check $n
        math::fibonacci $n
    } $n]
}]]
$computation done display display_error

There really isn't a huge difference in complexity and you get all the benefits of async computation. At the same time, consider how much more complex it would be writing an async version without promises and handling all the failure cases.

To summarize, the basic form of promise based computation has the pattern

  • Create a promise that initiates an async computation

  • Call its done method passing it a fulfillment reaction and a rejection reaction

Now we said the second argument to done, the rejection reaction, was optional. So what happens if it is not specified? To answer that we have to first describe how errors are handled when promises are chained.

Error handling in promise chains

Again, if you are not familiar with promise chaining via the then method, please read the Promises by Example post.

Assume we want to run a task on the successful completion of which we run a second task. Moreover, we want to handle any errors that may occur in either task. Here is a procedure defined to do that.

proc run2 {script1 script2} {
    set prom1 [ptask $script1]
    set prom2 [$prom1 then [lambda {script val} {
        puts $val
        then_chain [ptask $script]
    } $script2]]
    $prom2 done puts [lambda {reason error_dictionary} {
        puts "Oops! $reason"
    }]
}

Let us try it out.

% run2 {return "Task 1 completed"} {return "Task 2 completed"}
→ Task 1 completed
  Task 2 completed

The first task completes successfully and fulfills the value of the first promise whose fulfill reaction runs and initiates the second task. Since that also completes successfully, its fulfill reaction (the puts) is invoked.

What happens if the first task completes but the second one fails?

% run2 {return "Task 1 completed"} {error "Task 2 failed!"}
→ Task 1 completed
  Oops! Task 2 failed!

Again, the first task completes, but this time the script passed as the second task raises an error. As a result, the reject reaction of the second promise (the anonymous procedure) is invoked.

Finally what if the first task itself fails?

% run2 {error "Task 1 failed"} {return "Task 2 completed"}
→ Oops! Task 1 failed

We see that the failure of the first task is funnelled through to the reject reaction of the second promise. Here is the crucial statement.

set prom2 [$prom1 then [lambda {script val} {
    puts $val
    then_chain [ptask $script]
} $script2]]

The then method call on the first promise registers a fulfillment reaction to be called when it is fulfilled. However, the second argument, the rejection reaction, is unspecified. As a consequence, when the promise is rejected, there is no rejection reaction registered and the new promise returned by then is chained to the first promise and reflects its status which in this case is the rejection and its associated error value.

Thus all errors, no matter which async operation they occurred in, are all handled in a single place. The convenience of this cannot be overstated. This feature is what allows common error handling for a sequence of async operations with almost the same clarity as afforded by catch or try in the sequential world. In traditional async programming, error handling for our simple example would not be too hard but in anything more complex with multiple parallel operation fan-ins and fan-outs, it quickly becomes unmanageable. No so with promises.

Backgrounding errors

What happens to errors if an application has not registered any relevant rejection reactions, i.e. either directly or chained. In this case the error is passed on to the Tcl/Tk background error handler for the interpreter. This is the command returned by the interp bgerror command.

Note that error are backgrounded only if at least one applicable fulfillment reaction is registered but not any rejection reactions. If there are no applicable reactions (fulfillment or rejection) then the promise assumes the application is still to register its interest in the outcome and does not invoke the background error handler.

Errors in constructors

As we saw above, errors in promise-based async code result in the applicable rejection reactions being invoked. It is important to note that this applies to errors in the promise constructor as well. This, as noted in the ECMA specification, is required so that all errors can be treated uniformly no matter at which point they occur.

For example, here we call the ptimer command to create a promise based timer passing an invalid value.

% proc print_error {reason edict} {puts $reason}
% set timer [ptimer notaninteger]
→ ::oo::Obj62
% $timer done puts print_error
→ Invalid timeout value "notaninteger".

Notice that the ptimer call does not raise a Tcl exception. The errors are funnelled back using the standard promise reaction mechanisms.

As a more explicit example, here is a raw promise that errors out in its constructor.

% set prom [Promise new [lambda {prom} {
    error "You shall never construct me!"
}]]
→ ::oo::Obj63
% $prom done puts print_error
→ You shall never construct me!

The same error trapping also happens for the then method (which returns a promise) but not for done which does not return a promise.

In effect, you can construct and chain promises without worrying about having to trap errors at each stage.

Trapping of errors by reactions

Note that when a rejection reaction is invoked in response to an error, the error is not automatically propagated. It is up to the reaction to explicitly propagate the error or to handle it itself.

Here is an example where the exception generated by the ptimeout command is caught and handled.

set prom1 [ptimeout 1000]
set prom2 [$prom1 then "" [lambda {reason edict} {
    return "Timer expired but so what"
}]]
$prom2 done puts

And the equivalent when the error is propagated.

set prom1 [ptimeout 1000]
set prom2 [$prom1 then "" [lambda {reason edict} {
    error "$reason Arghh!"
}]]
$prom2 done puts print_error

Rejecting promises

So far we have looked at how rejections resulting from errors are handled. Now we look at flip side - how promises are rejected.

We have already seen through examples above that errors in async operations invoked through promises are caught and result in the promise being rejected. Promises can also be rejected explicitly as shown in the examples below.

Promises can be explicitly rejected by calling their reject method. So the example earlier that generated an error in the constructor could also have been written as follows:

% set prom [Promise new [lambda {prom} {
    $prom reject "You shall never construct me!"
}]]
→ ::oo::Obj63
% $prom done puts print_error
→ You shall never construct me!

In the above example, the constructor script is passed the promise object being constructed as the prom parameter and explicitly invokes the reject method on it.

In the case of the then method however, the promise object being constructed is not directly accessible from the reactions. In this case the reactions can use the then_reject procedure call to do an explicit rejection. See the reference documentation for details

The catch method

The catch method for promises is syntactic sugar for the then method when no fulfillment reaction is to be specified. It makes the intent of the method call clearer. Thus

$prom catch REJECTIONHANDLER

is exactly equivalent to

$prom then "" REJECTIONHANDLER

In conclusion

To summarize the discussion above, async programming is significantly complicated by the need to handle errors. In an event and callback based implementation, the error handling is spread amongst multiple code paths and difficult to follow. Moreover, there is no clear mechanism by which errors in an operation can be propagated when multiple operations are involved.

Much like the use of try in sequential code, promises simplify error handling in async code, at the same time reducing clutter and providing clarity.

comments powered by Disqus