Article
· Oct 14 9m read

What's the Catch?

Many programming languages use the try-and-catch construct to handle runtime errors gracefully. If the code within the try block encounters an error, it will throw an exception to the catch block, where the error handling occurs. Today we will dive into the ObjectScript implementation of this construct and discuss some ways to clean things up.

ObjectScript Implementation Basics

To get started, we will first look at the basic structure of the try/catch block:

try{
    //some code here
}
catch ex{
    //some error handling here
}

In this case, ex is a variable name for the exception object that has been thrown. You may utilize any name you wish for this variable.

If you are familiar with catch handling from other programming languages, remember that some features you may have used before are unavailable in ObjectScript. 

FirstFrst of all, there is no “finally” block there. In other languages, the "finally" block defines a piece of code that will run whether or not there is an exception in the "try" portion of the block. Still, you can easily work around it simply by following the "catch" block with more code. As long as there is no direct return from the try/catch, that code will be executed.

Secondly, you can not define multiple catch blocks for one try block to detect different kinds of exceptions. A common usage of this in Java, for example, is to have diverse catches for a few specific types of exceptions, and one more for all other exceptions later on. Instead, you can operate the exception object’s %IsA method to handle various other methods differently. That method takes a string as an argument, and if the exception is the one you are looking for, it returns a 1. Therefore we could handle certain exceptions diversely as follows:

catch ex{
    If ex.%IsA(“%Exception.StatusException”){
        //status exception handling here
    }
    elseif ex.%IsA(“%Exception.SQL”){
        //SQL exception handling here 
    }
    Else{
        //all other exception handling here
    }
}

Try/catch blocks can be nested inside one another. If an exception is thrown from the inner catch block, it will be handled by the outer catch block. If an exception is thrown from the last catch block, it will be managed by the next appropriate error handler if one exists. Issuing an argumentless quit command will exit the current try/catch block and send you back to the previous stack levelleve.

The Exception Object

Speaking of the exception object, let's take a closer look at what it is. There are several kinds of exceptions available on the market right now, but you can also extend those classes to create your own. All of them extend with %Exception.AbstractException besides the %Exception.CPPException which is meant for InterSystems internal use only. If you decide to make your own exception class, it will probably extend with %Exception.AbstractException as well. This class contains several useful properties and functions common for all exceptions.

The Name property typically has such exception names as <DIVIDE>, <NOLINE>, <UNDEFINED>, etc. The code contains the error code associated with the error. Both Name and Code can be helpful in catch blocks when trying to handle specific kinds of errors differently. For instance, if the Name is “<DIVIDE>”, you have divided by zero somewhere in the try block. If you know where that happened because you anticipated it, and you want to set the variable to zero, your catch block might look like the following:

catch ex{
    If ex.Name = “<DIVIDE>”{
        set myvar = 0
    }
}

The Location property might contain the spot where the error occurred. I have emphasized the word might since exceptions thrown manually via the throw command may not include this information. Exceptions thrown due to errors during code execution will have Location set to a routine, and they will offset letting you know where the error occurred. This information is practical for debugging. 

The InnerException property is an exception itself. It is usually set when you are already in a catch block and throw another exception. In that case, the InnerException of the second exception should be set to the original exception that got you to the first catch block.

The DisplayString method returns a string describing the error that caused the exception to be thrown in the first place. If you need to notify the user via your UI that an error has occurred, this is a worthwhile string to show him or her. The AsStatus method creates a %Library.Status object based on the exception which can come in handy if you are using a method that needs to return a status. The Log method adds the exception to the application error log found in the System Management Portal under System Operation, System Logs, and Application Error Log. Some methods also return the SQL code and SQL message, which is mostly advantageous if the exception is an SQL one.

Throw

If you want to catch something, it needs to be thrown first. Many exceptions are caught and thrown automatically during code execution. However, if you want to throw one manually, you can use the THROW command. The object you throw must be of a class inherited from %Exception.AbstractException though. It is beneficial for familiarizing yourself with the constructors of various exception classes and will help you later generate them when needed. Take a look at some of the most useful ones below:

THROW ##class(%Exception.GeneralException).%New(name,errnumber,location,data)
// Where name is the error name, e.g., <DIVIDE> or <UNDEFINED>; errnumber is the error code; location is the location in the code where the error occurred, and data is any additional data you want to send along.
THROW ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE)
// Where SQLCODE has been set by a query.
THROW ##class(%Exception.StatusException).CreateFromStatus(sc)
// Where sc is a %Library.Status object.

Like many other ObjectScript commands, it can take a postconditional expression and only throw the exception if that expression is true. For example, if you have a %Status object called sc, the following lines would throw a status exception based on that status only if the status is an error, or it would throw an SQL exception only if SQLCODE indicates an error:

THROW:($$$ISERR(sc)) ##class(%Exception.StatusException).CreateFromStatus(sc)

THROW:(SQLCODE<0) ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE)

THROW will send the exception to the successive error handler to be processed. When it is inside a try block, it will go to the catch block. If it is outside a try block, it may be a $ZTRAP. In that case, the exception object will be stored in the special variable $THROWOBJ for your use.

Issuing a THROW without an argument will re-throw the current system error stored in $ZERROR, passing it to the following error handler. However, it is generally not recommended because it can cause unexpected behavior if the current error handler changes $ZERROR directly or by causing another unintentional error.

Catch and Clean

You have caught your first exception. Now what?

In many cases, you will select to undo whatever you were testing in your try block when there is an error. It is a perfect case for using transactions. They start with a TSTART command. Once it has been issued, changes to your data will not be committed until you issue a TCOMMIT command. If you decide not to commit your changes, use the TROLLBACK command to undo them. You do not wish to leave a transaction hanging open, so if you use TSTART in your try block, you will need to ensure you employ TROLLBACK in your catch. If the TSTART is not the first thing in your try block, you can always check $TLEVEL to see if you have any active transactions. If you are concerned that there may be other transactions started outside of your try block, you will want to provide the number of transactions to roll back as well. The following might be a good basic form for your try/catch:

try{
    TSTART
    //do things here
    TCOMMIT
}
catch ex{
    If $TLEVEL > 0{
        TROLLBACK
    }
}

If your process uses any locks, you may also need to manage them. Locks are usually released when a process ends. However, if you create any locks that interfere with the current process, it will be better to unlock them. The easiest way to release all locks is to operate the LOCK command without arguments. Yet, it is typically recommended to unlock specific locks individually as needed.

Depending on your requirements, you might wish to inform the user that an error has occurred in a human-readable manner. As previously mentioned, the exception’s DisplayString() method gives a somewhat useful message to demonstrate to the user. If shown to a human, that string can be displayed on their screen through something similar to a pop-up. If it is an API, you may decide to include the exception’s Code property as well. It will allow the software calling the API to handle errors more easily since it will be consistent. However, the display string may change due to localization.

If your try/catch block is a method that must return a value, you need to ensure you return an appropriate value in order not to cause any errors further up the stack. If your method returns a status, your perfect choice would be the exception object’s AsStatus method. You can simply use “return ex.AsStatus()” at the end of your catch block to return an appropriate status. Otherwise, you will want to return an appropriate value of the type that the method is defined to return. Be aware that you can not use the QUIT command with an argument in a try/catch block. You must utilize return instead.

As a programmer, you might also wish to log the error for further inspection in case of a programming error that needs to be addressed. The previously mentioned Log method is a great place to start. It includes all information that is a part of the exception comment and allows you to add a comment to the error. You can see the code line (unless the error was thrown using the THROW command), the namespace, the error name, the device, the stack, the routine name, the security roles the process had, and many other things from that line alone.

If you want to dig a bit deeper, you may find a few useful things in the %SYS.ProcessQuery class. It includes such practical details as the client executable name and the user’s operating system user name. To get the information about the current process, you may use the following:

set myproc = ##class(%SYS.ProcessQuery).%OpenId($J)

Some properties, e.g., MemoryAllocated, MemoryPeak, and MemoryUsed might help you diagnose an issue caused by hardware limitations. Several other properties, however, turn out to be less helpful than they seem at first sight. For instance, items like CurrentLineAndRoutine would give you the line where you are referring to this property in your catch block, but not the one where the error occurred. 

On the other hand, the GetVariableList method can come in very handy. This method can obtain a list of variables defined in the process right now. Once you know which variable has been defined, you can log this information, and that can be very helpful indeed! This method requires you to pass a variable by reference that contains a list of lists. For its part, it returns an integer telling you how many sublists that list has. Each sublist includes two items: a variable name and the value of $D(variable) which tells you whether or not that variable holds any data. Using the MyProc mentioned above as a starting point, consider the following code:

Set x = myproc.GetVariableList(,.mylist)
For i=1:1:x{
    Set mysublist = $LG(mylist,i)
    If $LG(mysublist,2){
        Set myvar = $LG(mysublist,i)
        W myvar,!,@myvar
    }
}

The above-mentioned code will write out the variable names and values for every defined variable in the current process. Of course, instead of writing them down, you may choose to store them somewhere in a table or global for your further examination. It is a treasure trove of useful information. It can show you what variables may have been set to 0, which you did not account for, or if a variable that had to be defined never got defined. You can also see if something was supposed to be a number but ended up as a string. From my experience, you might find out that you misspelled a variable name somewhere, leading you to unanticipated results because the variable “tax” was initialized to 0, but the variable “txa” was the one where the math actually happened.

I hope this information will help you improve your clean-up process, inform users about an issue, and diagnose the root cause of your caught exceptions.

Discussion (1)2
Log in or sign up to continue

A quick comment on performance.  A TRY ... CATCH is very efficient at run time if no exception occurs.  The only extra code that is executed is a jump at the at end of the TRY block to go around the CATCH block.  However, TRY ... CATCH is slower than using the %ZTRAP when an execption does occur.  At compile time TRY ... CATCH blocks build tables that include program counters that describe which object code locations are in each TRY block and where the corresponding CATCH block begins.  When an error is signaled (or THROWn) at run time, it is necessary to scan the frame stack searching the compile-time generated TRY ... CATCH tables to find the CATCH location to go to after popping the intermediate stack frames.  If there are no TRY ... CATCH blocks involved then it is usually faster to just jump to the most recent setting of the $ZTRAP variable.

When exceptions are "exceptional"  then TRY ... CATCH is most efficient since it avoids pushing $ZTRAP value in preparation for handling an exception and it also avoids popping the $ZTRAP value when the exception does not occur.  If exceptions are part of a usual, successful, and often executed code path then it might be better to use $ZTRAP to catch those frequently executed signals.