logo
The Falcon Programming Language
A fast, easy and powerful programming language
Location: Home page >> Falcon wiki
User ID:
Password:
 

Falcon Wiki - Survival:Error recovery


Error recovery

You'll remember our first interactive Falcon script: that was the one asking you for your age and then entering a loop congratulating with you many times for your past birthdays. 

The int() function tried to convert a string (what you typed) into an integer (your age), but if this was not possible for some reason, a runtime error appeared instead, and the program was terminated. Here follows a reduced version of that script that will serve our needs:

print( "Enter your age: > " ) 
age = int( input() ) 
 
count = 0 
while count < age
   count += 1
   printl( "Happy belated birthday for your ", count, "." )
end 

Falcon provides a mechanism to handle unexpected situations that may arise in a program. Many library functions and language constructs use this mechanism to communicate with the controlling script about unexpected situations, but this system is also available to the script itself, so that script writers can take advantage of this. It's called exception raising .

Every time the Virtual Machine, one of the library functions or even other script parts run into a potentially dangerous situation, they raise an exception. If this exception is not handled somehow by the script, it is handed back to the system; the Falcon interpreter will print an error message and exit.

If the Falcon Virtual Machine is used by an embedding application to run some scripts, the embedder has the ability to set a top level exception handler. This will usually grant the embedding application the ability to know about fatal errors in the scripts, and take sensible actions (as i.e. mailing the administrators).  

There are a set of exceptions that are called unstoppable . These exceptions are raised by library functions or by the Virtual Machine itself if it finds some critical condition that may prevent scripts from working, as for example script bytecode corruptions. In those situations, letting the scripts intercept the exceptions would not be wise, hence the need of unstoppable exceptions.

Exceptions can be handled by the script by using the try - catch control block:

try 
   [try statements]
[ catch [object_type] [ in error_variable] ]
   [ catch statements ]
end 

Each catch block can intercept a certain kind of variable. The working principle is the same as the select statement; a type can be one of the type names, or it can be the name of a symbol declared somewhere in the program.

Try-catch blocks can be nested (put one into another) or combined with any other Falcon block statement ( if, while, for, function and so on). The try-catch block functionality is as follows: whenever an instruction inside the try (try-statements) causes an exception to be raised, the control flow is immediately broken. If a catch block is present, the type of the raised object is matched against the type specifiers of the catch blocks. Overall types (as i.e. StringType or ObjectType) get precedence, then the specific symbols used as specifiers are considered in the order they are declared in the catch clauses. For this reason, catch blocks intercepting subclasses should be declared before the ones intercepting parent classes. Finally, if none of the typed catch blocks matches the raised exception, the raised error is passed to a catch hander without type declaration, if present. If a typeless catch clause is not present, the error is then raised to the application level and this usually terminates the script.

The following example ensures that the user will write a numeric entry: 

age = 0 
while age == 0
   print( "Enter your age: > " )
 
   try 
      age = int( input() )
   catch 
      printl( "Please, enter a numeric value" )
   end 
end 

A catch clause may have an optional variable that will be filled with the exception that has been raised in the try block. The exception can be any Falcon item (including numbers, strings and objects) that describes what exactly was the error condition. By convention, the Virtual Machine and all the library functions will only raise an object of class Error , or one of its subclasses. However, scripts and other extensions libraries may raise any kind of item.

The Error class provides a series of accessors, that is, methods that are specifically used to access data in the inner object. Normally, scripts are not very interested in peeking the data inside an Error instance; usually, the embedding application is the entity that is meant to intercept errors and deal with them. For this reason, the embedding API puts at library disposal a C++ class called Falcon::Error; in case the script wants to intercept it, and only in that case, the C++ object is wrapped in Falcon object, and methods are used to query the internal Falcon::Error C++ instance. This is because intercepting and analyzing Error instances from scripts is considered an extraordinary operation; the overhead introduced by using methods instead of plain properties to retrieve Error values is marginal with respect to the advantage the embedding application receives by being able to use directly C++ objects in its code when a forbidding error condition is encountered by the script. 

The content of an Error Objects is enumerated in the Function Reference manual. Please, refer to that guide for the details. 

Now we can print a more descriptive error message about what the user should do in our test program: 

age = 0 
while age == 0
   print( "Enter your age: > " )
 
   try 
      age = int( input() )
   catch in error   // any variable name is ok here
      printl( "Oops, you caused the error number ", error.code,
            "\nwhich means that: ", error.message )
      printl( "Please, enter a numeric value" )
   end 
end 

Do not confuse the Error class with the above error variable: Falcon is fully case-sensitive, so the variable we named error in the above code is just a normal variable receiving an Error class instance.

Notice that the catch block is not immune to error raising. If an exception is raised inside a catch block, it will have exactly the same effect as if it were raised in any other part of the program: it may be caught again with another try/catch block, or it may be left to handle to the above handlers, or finally to the Virtual Machine. We'll see in a moment how this fact can be useful.

The try instruction can be abbreviated with the : operator; it wont be possible to catch any error in this case, but this may be useful in case any possible error must simply be discarded:

    try 
       age = int( input() )
    end 
   
   // is equivalent to
 
    try: age = int( input() )

Raising errors

It is interesting to be able to raise errors; the execution flow is immediately interrupted and a possible error manager is invoked, so raising errors inside the scripts may often obviate the need for "if" sequences, each of them checking for the right things to be done at each step. The keyword raise makes an item to be thrown and treats it as an exception.

The script may choose two different approaches to raise errors: one is that of creating an instance of the Error class using the Error() constructor, which accepts the following parameters:

Error( code, message, comment )

However, sometimes it is useful to throw a lighter object. Suppose that we want to set a maximum and minimum age in our example, and that we cause an error to be raised when those limits are not respected. In this case, that we may call flow control exception raising , having a full error to be raised may be an overkill. Follow this example:

age = 0 
loop
   print( "Enter your age: > " )
 
   try 
      age = int( input() )
      if age < 3: raise "Sorry, you are too young to type."
      if age > 150: raise "Sorry, age limit for humans is 150."
 
   catch StringType in error  
      printl( error )
      // age has been correctly assigned. Change it:
      age = 0
 
   catch Error in error
      // it's a standard error of Error class, manage it normally
      printl( "Oops, you caused the error number ", error.code,
            "\nwhich means that: ", error.description )
      printl( "Please, enter a numeric value" )
 
   catch in error
       printl( "Something else was raised... but I don't know what..." )
       printl( "So I raise it again and the app will die." )
        raise error
    end 
end age != 0

In this way, we have a controlled interruption of the normal code flow which is passed to the StringType catch branch, with a minimal overhead with respect to the equivalent code performed with a series of branches. If the weight of those branches becomes relevant, the exception code flow control may be even more efficient (the virtual machine management of try-catch blocks is comparatively light with respect to any other kind of operation), while it may be more elegant, and possibly more readable. 

It is also to be noticed that the caught variable may be parsed through a select statement. This may or may be an interesting opportunity, depending on the needed flexibility. The above code is equivalent to the following: 

   // the rest as before...
 
   try
      age = int( input() )
      if age < 3: raise "Sorry you are too young to type."
      if age > 150: raise "Sorry, age limit for humans is 150."
 
   catch in error  
      select error
          case StringType
            // manage strings as before
 
          case Error
            // manage Error instances as before...
 
          default 
            // print something as before... 
            raise error
      end 
   end 

This solution is a visually a bit less compact, requiring three indent levels where the previous only needed one. Also, the VM has an opcode that manages a typed catch a bit faster than a select statement (it's one VM opcode less, actually, but the opcode that is skipped with the typed catch approach is quite fast to be executed). However, it presents two advantages: first of all, it is possible to execute some common code before or/and after any specific error management. Secondly, the select code may be  delegated to a function (or to a lambda) that may be changed on the fly during program execution, actually changing the error management policy for that section. Through this kind of semantics, a common error management policy may be given to different handlers. As this doesn't prevent writing specific typed catches, each error management code may be highly customized through a combination of static typed catch statements and dynamic catch-everything statements passing the raised value to a common manager.


Navigation
Go To Page...

Loading

Elapsed time: 0.026 secs. (VM time 0.022 secs.)