Exceptions are abnormal events that occur while the program is running such as divide by 0 overflow, array index over bounds, nonexistent contract call, etc.
A well-designed program should provide methods for handling exceptions when they occur, so that the program does not block or produce unexpected results because of the occurrence of exceptions. For example, if a payment is insufficient or fails, we need to resume other operations.
Exception Handling Mechanism
Before the exception mechanism was introduced, exceptions were often handled by if-else. However, this method of handling exceptions is troublesome. If the same exception or error occurs in more than one place, the same processing should be done everywhere, resulting in many redundant codes with the same function.
Now, the common way is to use the try-catch mechanism to handle exceptions. The basic principle is as follows:
// Watch the area that might cause exceptions
// Catch and perform an exception handling operation
// Whether or not an exception occurs, the code will be executed
Some special situations：
1) If there is only a try-catch structure without finally segment. When another exception occurs during the execution of the catch segment, the new exception will be thrown directly to the outer layer and the execution of the segment is terminated.
throw error // This new error will be throwed outer layer
2) If there is only a try-finally structure without catch segment. When another exception occurs during the execution of the finally segment, the new exception will be thrown directly to the outer layer and the execution of the segment is terminated.
throw error //This error will be throwed outer layer
3) If a return statement is included in both the try and finally segments, the return in the finally segment will eventually be executed, but the return value can either be the value of the finally or the value of the try segment, which depending on the implementation of the language.
The try-catch exception handling mechanism supports nesting. If the exception is not currently caught, it continues to be thrown out of the call stack until it is eventually caught, or the program terminates execution because the exception is not caught.
Exceptions are often classified as follows：
- Error, indicates a serious problem during operation which can’t be handled by this program. Most of the errors have nothing to do with the code writer, but are problems with the code as it runs and often result in the program not being able to recover, such as memory overflow, insufficient memory, stack overflow and so on.
• Exception, which can be handled by the program itself, subdivided into the following two categories:
– RuntimeException, Runtime exceptions, which the program can choose to catch or not to catch. These exceptions are generally caused by the program logic errors, the code writer should try to avoid such exceptions. When a runtime exception occurs, if it is not caught, the system will throw it layer by layer up to the top. And if it is not caught, the execution of the program will exit. Common types: null-pointer exception, array over bounds, contract nonexistence, divisor zero, etc.
– NonRuntimeException, All exceptions except RuntimeException, such as those defined in other contracts, and some exceptions defined by contract developers. These exceptions are typically thrown by invoking the ‘throw’ instruction.
Exception Handling Features in Blockchain
Compared with a single language program, a public blockchain is often implemented in several different languages, so it is particularly important to ensure the certainty and consistency of exception triggering in different languages and platforms.
In the previous introduction, exceptions divided into two categories, one is runtime exceptions that the VM encounters internally. The other class is a non-runtime exception, it typically thrown manually by the user via the ‘throw’ instruction. This kind of exception is triggered with a certain execution path, belonging to a certain, which is defined.
In Neo3-VM, it mainly deals with non-runtime exceptions. In addition, we have define a catchable exception abstraction class: CatchableException, which can expand different types of exceptions according to needs in the future.
Principle of Exception Handling Mechanism of Neo3-VM
In Neo3-VM, we use the following mechanism for exception handling. The TryContext context is added to record the current Try information, including the starting position of each segment of the try-catch-finally and the state of the current TryContext context. There are three types of states: Watching, Catching, and Cleaning. The TryStack is added to the current ExecutionContext, which record the try-catch information encountered during the current execution process, it also supports try-catch nesting.
When an exception occurs, it is looked up from the Try stack of the execution context at the top of the invocation stack. If there is a try-catch that can catch the exception, the catch segment or the finally segment will be executed. Otherwise, the stack is dequeued until the corresponding try-catch is encountered or the VM execution is terminated.
• If the exception occurs in the catch segment, then the finally segment is executed, and the exception is thrown out again, the current trycontext is droped.
• If the exception occurs in the finally segment, it is thrown directly and pop the current trycontext from try stack.
In the instructions of neo-vm, try-catch contains four instructions in total and three throw instructions, respectively with the following functions:
TRY catchOffset, finallyOffset // try-catch-finally if no catch segment, set catchOffset 0; if no finally segment, set fianllyOffset 0
ENDT // end try
ENDC // end catch
ENDF // end finally
THROW // It will throw an vm exception
THROWIF bool // Pop the top element in stack, if true, then throw an vm exception
THROWIFNOT bool // Pop the top element in stack, if false, then throw an vm exception
• When TRY is encountered, a new TryContext is created in the current execution context, and the address of catch, finally sections is set, and the current state is set as Watching.
• When ENDT or ENDC is encountered, it will jump to fianlly segment for subsequent processing. If the finally segment does not exist, then pop the current TryContext and continue to execute.
• When ENDF is encountered, then pop the current TryContext and continue to execute.
Note: if the RET instruction appears in the TRY segment, it does not go to finally segment anymore, but returns directly. The compiler needs to be careful when compiling the upper level language, such as C#, which turns the return instruction into a JMP instruction.
An example of a neo3-vm opcode program with try-catch nesting：
01: 0x0b0e // catch -> 0b, finallyOffset = 0e
0b: PUSH2 // catch position
0c: ENDC // end catch
0e: PUSH3 // finally position
0f: ENDF // end finally
In the internal try-catch, when the try segment is executed, the user will actively throw an exception due to the throw instruction and be caught to execute the catch segment. However, during the catch segment, a second throw instruction is encountered. At this point, the internal try-catch has no finally segment, and the new exception is simply thrown out.
The outside try-catch catches the second exception and jumps to the catch segment, performing PUSH2. When the catch segment finished, continues to jump to the finally segment, performing PUSH3, and finally ENDF to end the exception handling.
Finally, the data in the evaluation stack is: 3 at the top of the stack and 2 at the bottom of the stack.
This scheme is currently in the code merge phase and does not represent the final solution.
By Chuan Lu