Defer Block and Error Handling


The error handling in Dao is based defer blocks which can be executed unconditionally or
conditionally with respect to exceptions.

 0.1   Defer Block  

A defer block is a block of code that can be executed when a function returns normally or
exits due to excpetions. Its execution can be conditional or unconditional with respect t
o exceptions: 
  *  defer{block} or defer(){block}: unconditional exection,  when a function exits with 
     or without exception; 
  *  defer(none){block}: conditional execution, only when the  function exits without exc
  *  defer(any){block}: conditional execution, when the function  exit with any exception
  *  defer(type){block}: conditional execution, only when the  function exit with excepti
     on(s) matching to type; 
  *  defer(type as name){block}: the same as defer(type){block} except that the exception
     object will be accessible by name in the block. 

Unconditional defer blocks are mostly useful to do cleanups that need to be done regardle
ss whether and what errors might happen in the surrounding function. And the defer blocks
that are conditional on none might be useful to support transactional like operations to 
perform certain operations only when no error happens. And the other conditional defer bl
ocks are mainly useful for error handling.

A defer block is compiled as a closure, so it can access outer scope constants and variab
les in the same way as closures. These outer scope variables are captured at the time the
defer block is reached in the normal execution. When a function exits, all the executable
defer blocks that have been reached in the normal execution will be executed in the rever
se order of being reached.

   1  routine Test()
   2  {
   3      defer {
   4          io.writeln( "always executed" )
   5      }
   6      defer( none ){
   7          io.writeln( "defer( none )" )
   8      }
  10      for(var i = 2 : 5 ) defer { io.writeln( "deferred", i ) }
  12      io.writeln( "returning" );
  13      return 123
  14  }
  16  io.writeln( Test() )

 0.2   Error Handling  

The main way to handle errors in Dao is to use conditional defer blocks. Different condit
ional defer blocks can be defined for different error types. Each conditional defer block
can only be activated once by one exception object, and the more recent exception object 
is checked first if there are multiple exception objects.

To handle an error of certain type, one need to define a defer block with the error type 
as its parameter. Such that the block will only be executed when an error of that type ac
tually happened. Then the error object will be passed to the defer block for proper handl
ing. The error is suppressed by such defer block to allow program procede normally (after
returning to its caller).

Such defer block must return or not return value in the same way as the routine where the
defer is defined. The returned value by such defer will by used as the new return value o
f the routine.

   1  routine Test()
   2  {
   3      defer ( Error ){
   4          io.writeln( "Error is handled! And a new value is returned!" )
   5          return 456
   6      }
   7      io.writeln( "Test(): before error;" )
   8      std.error( "some error" )
   9      io.writeln( "Test(): after error;" )
  10      return 123
  11  }
  12  io.writeln( Test() )
Here the standard method std.error() is used to raise a generic error. This method has th
ree overloaded forms, and the simplest version can take a string as parameter to become t
he message of the error as shown in the example. Various errors can also be raised by the
Dao runtime for standard operations and methods.

 0.3   Error Types  

The Error type used in the above example represents a generel error type. It is also the 
base type from which all error types are derived. The Error itself is derived from the Th
e Exception type, which stores the essential information about an exception type. Such in
formation includes general name, error message, error data, source file path, line number
and function call stack trace.

The followings are the standard exception types: 
   1  Exception                # General exception
   2  Warning                  # General warning
   3  Error                    # General error
   4  Error::Field             # Invalid field
   5  Error::Field::NotExist   # Field not exist
   6  Error::Field::NotPermit  # Field not permit
   7  Error::Float             # Floating point error
   8  Error::Float::DivByZero  # Division by zero
   9  Error::Float::OverFlow   # Floating point overflow
  10  Error::Float::UnderFlow  # Floating point underflow
  11  Error::Index             # Invalid index
  12  Error::Index::Range      # Index out of range
  13  Error::Key               # Invalid key
  14  Error::Key::NotExist     # Key not exist
  15  Error::Param             # Invalid parameter(s)
  16  Error::Syntax            # Invalid syntax
  17  Error::Type              # Invalid type
  18  Error::Value             # Invalid value
  19  Error::File              # File error
Note that, Warning and Error are also registered in the global scope, so they can be dire
ctly used without Exception:: scoping. Among these exception types, only Exception, 
Warning and Error are  pre-defined, and the rest are only pre-declared. Only the pre-defi
ned exception types can be directly used in Dao code. Other error types need to be explic
itly defined using the Exception::Define() method in the following way, 
   1  const IndexError  = Exception::Define( "Error::Index" )
This is a constant expression which will be evaluated during parsing so that the defined 
type can be used right after this line of code. The evaluation of this method call will c
reate a new error type named Index that is derived from Error. This new error type will b
e created a C data type and scoped / registered inside the Error type. The scoped name pa
ssed to the Exception::Define() as the first parameter will serve as a unique string ID f
or the new error type, and the future error type definition using the same string will re
turn the same error type.

Cutomized error type can be defined either by defining a Dao class that is derived from t
he Error type, or by defining a C type using the above method, 
   1  const MyErrorType = Exception::Define( "Error::MyError", "General information" )
Here the second parameter is the general information that is associated to the error type
. Defining pre-declared error types need no such parameter, as it will be set internally.
Defining a new error type this way as a C data type has the advantage of convenience and 

Now the newly defined error type can be used in two ways, by either using the new constan
t value retuned by Exception::Define(), or by using the cannonical name Error::MyError, 
   1  defer( MyErrorType ) { ... }
   2  defer( Error::MyError ) { ... }

Here is another example of defining a new error type by subclassing from Error, 
   1  class MyError : Error
   2  {
   3      routine serialize(){ return ('MyError', self) }
   4  }

 0.4   Raising Warning and Error  

In Dao code, a warning can be raised simply with the standard method std.warning(), 
   1  std.warn( info: string )
This will print the warning message right away along with file name and file number where
the warning is issued. It also prints the call stack trace for this call to provide some 
information about the context where the warning happens.

An error can be similarily raised with the standard method std.error(), 
   1  std.error( info: string )
In Dao, raising an error will cause the current routine to exit immediately, and to start
to execute the defer blocks if the routine has created any. If one of the defer blocks ha
s handled the error, the execution will be resume in its caller. Otherwise its caller wil
l also exit with extra execution of its defer blocks as well. This will be repeated until
either the error is handled and the normal execution is resumed, or there is no more call
er. In this case, the error will be printed with a list of call trace including function 
names, file names and line numbers.

There are three variants of the standard error method. The above one is the first and sim
plest one. The other two are the following: 
   1  std.error( errorObject: Error )
   2  std.error( errorType: class<Error>, info: string, data: any = none )
The former will allow raising a pre-created error object. The later will create an error 
on-the-fly as an instance of the error type errorType with message info and data data. It
worthes noticing that errors of error types defined by Exception::Define() can only be ra
ised by the later method, because types defined this way have no constructors.

 0.5   Methods Related to Error Handling  

There are three standard methods that are related to error handling. 
   1  std.exec() [=>@T] => @T
   2  std.exec( defaultValue: @T ) [=>@T] => @T
   3  std.try() [=>@T] => list<Error>|Error|@T

 0.5.1   std.exec()[=>@T]=>@T  

As mentioned above, the normal execution will be resumed by the caller of the function th
at handles the exceptions in its defer blocks. In order to resume the normal execution ri
ght after handling of exceptions, a special type of code blocks can be used to "host" the
defer blocks, 
   1  std.exec() [=>@T] => @T
Since code sections are executed in new stack frames. Handling errors inside such code se
ction will allow the program to resume right after the code section calls.
   1  var fout = io::stdio
   3  std.exec {
   4      defer { recover() }
   5      fout = io.open( "NonExistentFile.txt", 'r+' )
   6  }
   8  if( fout != io::stdio ) defer{ fout.close() }
  10  fout.writeln( 'hello' )

 0.5.2   std.exec(defaultValue:@T)[=>@T]=>@T  

This method can be used whenever an expression or a block of code may either suceed and r
eturn the result, or fail and return a pre-defined default value. The passed in 
defaultValue will be returned only if the code section fails with exception. And in such 
case, the exception will be suppressed automatically. This method will allow simplifying 
the above example into the following, 
   1  var fout = std.exec( io::stdio ){ io.open( "NonExistentFile.txt", 'r+' ) }
   2  if( fout != io::stdio ) defer{ fout.close() }
   3  fout.writeln( 'hello' )

 0.5.3   std.try()[=>@T]=>list<Error>|Error|@T  

This method is similar to the above one, but instead of returning a predefined default va
lue, it will return the error object(s) if the code section failed with error(s).