swirl
Guide to OmniMark 9   OmniMark home
docs home 
IndexConceptsTasksSyntaxLibrariesLegacy LibrariesErrors
 
    Related Topics  

Catch and throw

You can use catch and throw to manage the execution flow in your OmniMark programs. 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 versus function calls

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:

Catching program errors

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 server loop of a simple server program.

  import "omtcp.xmd" prefixed by tcp.
  
  process
     local tcp.service my-service
     set my-service to tcp.create-service on 5432
     repeat
        local tcp.connection my-connection
        set my-connection to tcp.accept-connection from my-service
        using output as tcp.writer of my-connection 
           submit tcp.reader of my-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 my-service
     set my-service to tcp.create-service on 5432
     repeat
        local tcp.connection my-connection
        set my-connection to tcp.accept-connection from my-service
        using output as tcp.writer of my-connection 
           submit tcp.reader of my-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 the line "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 my-service
     set my-service to tcp.create-service on 5432
     repeat
        local tcp.connection my-connection
        set my-connection to tcp.accept-connection from my-service
        using output as tcp.writer of my-connection 
           submit tcp.reader of my-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 block 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 to catch #external-exception in the code above, because the failure of a program to catch 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 || "."

Suppose the attempt to open the named file fails. This causes an external exception 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.

Exception handling

When it comes to throwing things, OmniMark does not get to have all the fun. We can create our own exceptions, throw our own throws, and define our own catches. 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 my-service
     set my-service to tcp.create-service on 5432
     repeat
        local tcp.connection my-connection
        set my-connection to tcp.accept-connection from my-service
        using output as tcp.writer of my-connection 
           submit tcp.reader of my-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 is a request for information. Every request except one. 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". Catches 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 in the first line 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.

Throwing additional information

When OmniMark throws to #program-error or #external-exception, it adds additional information in the form of three parameters: code, message, and location. It is only fair that we be allowed to pass additional information with our throws as well. We can do this 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 my-service
     set my-service to tcp.create-service on 5432
     repeat
        local tcp.connection my-connection
        set my-connection to tcp.accept-connection from my-service
        using output as tcp.writer of my-connection 
           submit tcp.reader of my-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* => the-reason
     throw nap-time because the-reason

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.

Cleaning up after yourself

We said that OmniMark cleans up after itself, and it does. It 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" keyword. 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.

  declare catch nap-time because value string reason
  import "omtcp.xmd" prefixed by tcp.
  global stream log-file-name ;should be initialized from the command line
  
  process
     local tcp.service my-service
     local stream my-log
     set my-service to tcp.create-service on 5432
     repeat
        local tcp.connection my-connection
        reopen my-log as file log-file-name
        set my-connection to tcp.accept-connection from my-service
        put my-log tcp.peer-ip of my-connection || date "=Y/=M/=D =h:=m:=s"
        using output as tcp.writer of my-connection 
           submit tcp.reader of my-connection
        close my-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 my-log" was never executed and the log file would remain open. (This is something OmniMark can't clean up itself, since the stream my-log belongs to a wider scope which is not being closed.) We want the line "close my-log" to be executed always, whether there is an error or not. To ensure this, we use "always":

  declare catch nap-time because value string reason
  import "omtcp.xmd" prefixed by tcp.
  global stream log-file-name ;should be initialized from the command line
  
  process
     local tcp.service my-service
     local stream my-log
     set my-service to tcp.create-service on 5432
     repeat
        local tcp.connection my-connection
        reopen my-log as file log-file-name
        set my-connection to tcp.accept-connection from my-service
        put my-log tcp.peer-ip of my-connection || date "=Y/=M/=D =h:=m:=s"
        using output as tcp.writer of my-connection 
           submit tcp.reader of my-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 my-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* => the-reason
     throw nap-time because the-reason

When a throw happens, OmniMark closes scopes one by one until it finds a scope that contains a catch for that throw. As it does so, it executes any code in an always block in each of those scopes, including the scope that contains the catch. So in this example, the "close" line will be executed before the catch block 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 read and write. 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):

  declare catch nap-time because value string reason
  import "omtcp.xmd" prefixed by tcp.
  global stream log-file-name ;should be initialized from the command line
  
  process
     local tcp.service my-service
     local stream my-log
     set my-service to tcp.create-service on 5432
     repeat
        local tcp.connection my-connection
        reopen my-log as file log-file-name
        set my-connection to tcp.accept-connection from my-service
        put my-log tcp.peer-ip of my-connection || date "=Y/=M/=D =h:=m:=s"
        using output as tcp.writer of my-connection 
           submit tcp.reader of my-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 my-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* => the-reason
     throw nap-time because the-reason
     
  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"
                 )  

    Related Topics
 
 

Top [ INDEX ] [ CONCEPTS ] [ TASKS ] [ SYNTAX ] [ LIBRARIES ] [ LEGACY LIBRARIES ] [ ERRORS ]

OmniMark 9.1.0 Documentation Generated: September 2, 2010 at 1:35:14 pm
If you have any comments about this section of the documentation, please use this form.

Copyright © Stilo International plc, 1988-2010.