[NAME] ALL.dao.defer-error [TITLE] Defer Block and Error Handling [DESCRIPTION] 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 eption; * defer(any){block}: conditional execution, when the function exit with any exception (s); * 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 } 9 10 for(var i = 2 : 5 ) defer { io.writeln( "deferred", i ) } 11 12 io.writeln( "returning" ); 13 return 123 14 } 15 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. Example, 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 simplicity. 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 2 3 std.exec { 4 defer { recover() } 5 fout = io.open( "NonExistentFile.txt", 'r+' ) 6 } 7 8 if( fout != io::stdio ) defer{ fout.close() } 9 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).