You can use catch
and throw
to manage the execution flow in an OmniMark program. catch
and throw
is a powerful addition to the flow handling features of OmniMark, allowing you to make major
redirections of program flow in a safe and structured way.
It is probably easiest to think of catch
and throw
as an exception handling mechanism. What is
an exception? Essentially anything that is an exception to the normal flow of your processing. Some exceptions
come whether you want them or not, in the form of run-time program errors or failure to communicate with the world
outside your program. Others are simply an expression of the way you choose to solve your programming problem.
It is often the case that a simple piece of code can handle 90% of all cases. The other 10% are exceptions that require significantly different processing. Dividing a problem up into "normal" cases and "exceptions" is a convenient way to simplify and organize your code. This does not mean that the exceptions are errors or even unexpected. Often the exceptional cases are what you are most interested in. Programming with exceptions is simply a technique for designing an algorithm to solve a particular problem.
catch
and throw
have much in common with function calls. Both
declare and use parameters to pass information, and both cause a transfer in execution to a new part of the
program. They differ in the following principal ways:
throw
initiates the
collapsing of current execution scopes up to and including the scope in which the
catch
occurs.
overloaded
function body can be defined only once for a given function name. Multiple
different catch
clauses can be defined for the same catch
name, so long as they
occur in different lexical scopes. Which catch
clause is executed depends on which is active at
the time a throw
occurs. Only one catch
of a given name is active at any given
point in program execution.
catch
does not have to use the full syntax of its declaration when it is defined in
code. The names of the parameters used in a definition do not have to match those used in the declaration or
any other definition—only the position or the heralds have to match. A definition does not have to define
any of the parameters, but must define any it intends to use.
The simplest use of catch
and throw
is to allow your program to recover when something goes
wrong. Consider the following code, which contains the main loop of a simple server program.
import "omtcp.xmd" prefixed by tcp. process local tcp.service service initial { tcp.create-service on 5432 } repeat local tcp.connection connection initial { tcp.accept-connection from service } using output as tcp.writer of connection submit tcp.reader of connection again
Any error in the program or with the TCP connection will cause this program to terminate, since there is
nothing to handle the error. Server programs should be written to stay running if at all possible, so we need to
do something to allow the program to recover:
import "omtcp.xmd" prefixed by tcp. process local tcp.service service initial { tcp.create-service on 5432 } repeat local tcp.connection connection initial { tcp.accept-connection from service } using output as tcp.writer of connection submit tcp.reader of connection catch #program-error again
We have added only a single line, but this version of the program is much more robust. If any error occurs
while in the repeat
loop or any of the find
rules invoked by the submit
, execution will
transfer to catch #program-error
. Along the way, OmniMark will clean up after itself: local scopes
will be terminated, and resources released.
No attempt is made to salvage the work that was in progress when the error happened. That work, and all the resources associated with it, are thrown away. But the server will stay up and running and ready to receive the next request.
Of course, it would be nice to know that the error occurred and why it occurred. #program-error
makes
information available so that we can act on the error, or at least report it. To ensure that errors get logged,
we can rewrite the program like this:
import "omtcp.xmd" prefixed by tcp. process local tcp.service service initial { tcp.create-service on 5432 } repeat local tcp.connection connection initial { tcp.accept-connection from service } using output as tcp.writer of connection submit tcp.reader of connection catch #program-error code c message m location l log-message "Error " || "d" % c || " " || m || " at " || l || " time " || date "=Y/=M/=D =h:=m:=s" again
Note that the catch
line guards everything that follows from being executed. The only way into the code
of a catch
clause is to be caught by that catch
.
So far, we have seen nothing of the throw
part of throw
and catch
. This is because
OmniMark itself throws to #program-error
. OmniMark can also throw to #external-exception
if it
has a problem communicating with the external world, such as being unable to open a file or communicate
successfully with an opaque
external component. We don't bother handling #external-exception
specially in the code above, because failure to handle an external exception is itself a program error, which
causes a throw
to #program-error
. In other circumstances we might want to use
#external-exception
explicitly. Suppose one of the find
rules in our server program tried and
failed to open a file:
find "open file " letter+ => foo output file foo catch #external-exception output "Unable to open file " || foo || "."
If the attempt to open the named file fails, an external exception is thrown at the first output
action. We catch the exception and provide alternate output. The program then continues as if nothing had
happened.
Now our server is more robust still, since an error opening a file will not cause processing of the current request to be aborted. Instead, the client will receive the error message we output along with any other information the request generates.
We can create our own exceptions, throw
our own throws, and define our own catch
es. Let's write our own
exception to let us shut down the server by remote control (to simplify things, we've left out some of our
earlier enhancements):
import "omtcp.xmd" prefixed by tcp. declare catch nap-time process local tcp.service service initial { tcp.create-service on 5432 } repeat local tcp.connection connection initial { tcp.accept-connection from service } using output as tcp.writer of connection submit tcp.reader of connection catch #program-error again catch nap-time find "sleep" throw nap-time
Here we have a classic case of an exception. Every request to our server except one is a request for
information: the sleep
request is an instruction to the server to shut itself down. This is the exceptional
case and we handle it with an exception.
In the exceptional case that we are shutting down the server, we need to jump out of the connection loop. We
do this with a catch
outside the loop. The catch
is called nap-time
. catch
es
are named so that we can have more than one and have each one catch something different. Like all names
introduced into an OmniMark program, the name of a catch
must be declared, which we do with a declare
at the beginning of the program. We place the catch
outside the request handling loop.
Because OmniMark cleans up after itself while performing a throw
, all the resources belonging to the
local scope inside the loop are properly closed down. Then, since we are outside the loop and at the end of the
process
rule, the program simply ends, shutting down the server.
The throw
itself is very simple. Having detected the exceptional case (the sleep
request)
we simply throw
to the appropriate catch by name. OmniMark handles everything else.
When OmniMark throws to #program-error
or #external-exception
, it adds additional information
in the form of three parameters: code
, message
, and location
. We can add
information to our throw
s as well, by adding parameters to our catch
declaration, following the
form of a function definition. Let's add the capability to log the reason for putting a
server to sleep:
import "omtcp.xmd" prefixed by tcp. declare catch nap-time because value string reason process local tcp.service service initial { tcp.create-service on 5432 } repeat local tcp.connection connection initial { tcp.accept-connection from service } using output as tcp.writer of connection submit tcp.reader of connection catch #program-error again catch nap-time because r log-message "Shut down because " || r || " Time: " || date "=Y/=M/=D =h:=m:=s" find "sleep" white-space* any* => s throw nap-time because s
Here the find
rule captures the rest of the sleep
message and uses it as a parameter to the
throw
. The catch
receives the data and uses it to create the appropriate log message.
OmniMark cleans up everything it knows about. But this may still leave you with cleanup of your own to do. Or
there may simply be things that always have to be done, even if an exception occurs. For this, OmniMark provides
the always
clauses. Let's suppose that our server does its own connection logging, while still using
OmniMark's logging facility for errors. We want the connection log file closed between requests to make it easy
to cycle the log files, so we open and close the log file for each connection.
import "omtcp.xmd" prefixed by tcp. declare catch nap-time because value string reason global stream log-file-name process local tcp.service service local stream server-log set service to tcp.create-service on 5432 repeat local tcp.connection connection reopen server-log as file log-file-name set connection to tcp.accept-connection from service put server-log tcp.peer-ip of connection || date "=Y/=M/=D =h:=m:=s" using output as tcp.writer of connection submit tcp.reader of connection close server-log catch #program-error code c message m location l log-message "Error " || "d" % c || " " || m || " at " || l || " time " || date "=Y/=M/=D =h:=m:=s" again catch nap-time because reason log-message "Shut down because " || reason || " Time: " || date "=Y/=M/=D =h:=m:=s"
In this code, a problem processing the request would cause a throw
to #program-error
. This
would mean that the line close server-log
was never executed and the log file would remain open. This is
something OmniMark cannot clean up itself, since server-log
belongs to a wider scope which is not
being closed. We want the line close server-log
to be executed always, whether there is an error or not.
To ensure this, we use an always
clause:
import "omtcp.xmd" prefixed by tcp. declare catch nap-time because value string reason global stream log-file-name process local tcp.service service local stream server-log set service to tcp.create-service on 5432 repeat local tcp.connection connection reopen server-log as file log-file-name set connection to tcp.accept-connection from service put server-log tcp.peer-ip of connection || date "=Y/=M/=D =h:=m:=s" using output as tcp.writer of connection submit tcp.reader of connection catch #program-error code c message m location l log-message "Error " || "d" % c || " " || m || " at " || l || " time " || date "=Y/=M/=D =h:=m:=s" always close server-log again catch nap-time because reason log-message "Shut down because " || reason || " Time: " || date "=Y/=M/=D =h:=m:=s" find "sleep" white-space* any* => s throw nap-time because s
When a throw
happens, OmniMark closes scopes one by one until it finds a scope that contains a catch
clause for that throw
. As it does so, it executes any code in an always
clause in each
of those scopes, including the scope that contains the catch
. So in this example, close
server-log
will be executed before the catch
clause is executed, no matter where or why an error
occurs.
Programming with exceptions is a powerful technique that can make your programs both more reliable and easier
to maintain. Here is our full server program with all our catch
and throw
functionality, plus
logging of our external exception (but minus the find
rules that do the rest of the work):
import "omtcp.xmd" prefixed by tcp. declare catch nap-time because value string reason global stream log-file-name process local tcp.service service local stream server-log set service to tcp.create-service on 5432 repeat local tcp.connection connection reopen server-log as file log-file-name set connection to tcp.accept-connection from service put server-log tcp.peer-ip of connection || date "=Y/=M/=D =h:=m:=s" using output as tcp.writer of connection submit tcp.reader of connection catch #program-error code c message m location l log-message "Error " || "d" % c || " " || m || " at " || l || " time " || date "=Y/=M/=D =h:=m:=s" always close server-log again catch nap-time because reason log-message "Shut down because " || reason || " Time: " || date "=Y/=M/=D =h:=m:=s" find "sleep" white-space* any* => s throw nap-time because s find "open file " letter+ => foo output file foo catch #external-exception identity i message m location l output "Unable to open file " || foo || "." log-message "Error " || i || " " || m || " at " || l || " time " || date "=Y/=M/=D =h:=m:=s"