Discussion
· Sep 28, 2020

%Status usage in ObjectScript

Hi developers!

Want to discuss with you the case of %Status.

If you familiar with ObjectScript you know what is it. I'd love to hear the history of the case why it had appeared in ObjectScript but it turned out that almost every system/library classmethods return %Status and there is a whole set of tools to deal with it.

What is does it gives you the responsibility to check the value or %Status of every system method you call.

E.g. if you save the data of the persistent class,  you should never call like this:

do obj.%Save()

you need to call:

set sc=obj.%Save()

if $$$ISERR(sc) do // something or quit.

Or if you use try/catch approach in your code you use the following macro:

$$$THROWONERROR(sc,obj.%Save())

Which turns your code into something like:

set sc=$$$OK

Try {

$$$THROWONERROR(sc,##class(x.y).a())

$$$THROWONERROR(sc,##class(x.y).b())

$$$THROWONERROR(sc,##class(x.y).c())

$$$THROWONERROR(sc,##class(x.y).z())

do obj.NormalMethod(parameter)

}

catch e {

// error handling

}

Which makes the code look like a sequence of $$$THROWONERROR calls. Which I don't find very readable.

So, if you introduce a new %Status method, you make ALL the users of this method use either $$$THROWONERROR or $$$ISERR

And my questions are:

What is the value of %Status vs try/catch?

do you use %Status in newly introduced methods of YOUR solutions?

Why?

Do you create new methods with %Status?
Discussion (19)2
Log in or sign up to continue

I use %Status exclusively; I really, really don't like try/catch. The most important reason for me is that I want to add information about what went wrong to the status. In e.g. obj.%Save(), the returned status tells me that saving an object went wrong, and hopefully why. I want to add to this which object could not be saved, and possibly some other state that may be relevant for debugging the problem. I find this creates code that is easy to read and debug.

By the way, "If 'sc" is, to me, a lot easier on the eyes than "If $$$ISERR(sc)"...

You only need Try-Catch when you expect that something might go wrong and can't handle it upfront.
For regular error-handling, something that you anticipate never happens, the $ZTrap-ErrorHandler is much more elegant. It doesn't introduce extra stack-levels and identation.

%Status is an elegant way of letting the caller know something went wrong and leave it to it's discretion to do with it whatever is appropriate, the callee should have done anything to handle the 'error' properly, logging, recovery, whatever. It's a matter of seperation of concerns.

Thanks for the insights, @Herman Slagman!

$ZT + Label is better than try/catch

%Status is better than try/catch too.

why %Status is better than throw ("something went wrong") approach?

But, how to do you track errors of your solutions happen on a customer side? There is an elegant way to manage this with try/catch:

catch e {

 do e.log() // store the error and stack in Application Errors

// handle error

} 

How could you manage this with %Status and $ZT approach?

I don't know if try/catch is slow, and I don't care. I don't use it because it is too wordy to handle errors exactly where they occur. It encourages code like in your example, where the code in methods is wrapped entirely in a try/catch block. You say the error object has all the information, but I disagree. It has some low-level information, but often lacks the context I need to determine what the problem is.

My preferred way of handling %Status errors is to add a %Status in front of it with more details of what happened when the error occurred, and return this to the caller. Somewhere up the call chain something will then handle the problem, e.g. add something to the Ensemble event log. This is such a standard way of working for me that I created a macro specifically for prefixing the new status information.

For errors that "raise" I also prefer $ZTrap+$ZError; I don't see the added value of try/catch here either.

AFAIK Try/catch indeed slows a bit the execution (@Dan.Pasco is it true?)

And try/catch shouldn’t be presented in every method - it could be somewhere on top and in the places where you need to catch errors.

I like your answer but for better understanding it deserves a sample code to see how do you manage error scenarios.

we really lack of good templates for beginners on how to better handle errors in serious project.

I would not say that try-catch is slow. If you reach the end of a try block, it simply branches over the catch block. This is a very cheap operation. In the nominal case (i.e., no error or exception), the cost of a try block is probably less than setting a %Status variable, and it would take several try blocks to match the cost of setting $zt once.

The catch block does almost all of the work. It instantiates the exception object, unwinds the stack, etc. I don't know offhand how catch compares to $zt, but the performance of the exceptional cases is usually not as important.

As I recall, some of the conventions of the object library date back to Caché 2.0 in the late nineties, whereas try-catch wasn't introduced until Caché 4.0.

One arguable advantage of %Status-based interfaces is that they work well with the noisy %UnitTest framework. If you $$$AssertStatusOK on a series of method calls, the test log reads like a transcript of your activity. You can often understand the failure without even needing to read the test program.

Also, try-catch is kind of annoying to use without finally, which was never implemented.

Evgeny,
Is it a question: try/catch (or another way of error handling) or %Status? If we try to follow some solid design principles, each method should perform only one function, have only one way to exit from, and should inform the caller whether it succeded. So, we should use both. Extending your sample: 

ClassMethod solid1(parameter, ...) as %Status
{
  set sc=$$$OK
  try {
     $$$TOE(sc,##class(x.y).a())
     $$$TOE(sc,##class(x.y).b())
     ...
     $$$TOE(sc,obj.NormalMethod(parameter))
     ...
  } catch e {
     // error handling
     set sc=e.AsStatus()
  }
  // finally... common finalization for both (good or bad) cases
  return sc
}

One could write it in another fashion, e.g.

ClassMethod solid2(parameter, ...) as %Status
{
  set sc=$$$OK
  new $estack,$etrap
  set $etrap="if $estack=0 goto errProc"
  set sc=##class(x.y).a()) if 'sc goto solid2Q
  set sc=##class(x.y).b()) if 'sc goto solid2Q
  ...
  set sc=obj.NormalMethod(parameter) if 'sc goto solid2Q
  ...
solid2Q
  // finally... common finalization for both (good or bad) cases
  return sc

errProc // common error handler
  // error handling
  set sc=$$$ERROR(5001,"solid2 failed: "_$ze)
  goto solid2Q
}

Which version is better? If we don't need individual error handling of ##class(x.y).a(), ##class(x.y).b(), etc calls, I'd prefer solid1. Otherwise, solid2 seems to be more flexible: it's easy to add some extra processing of each call return. To achieve the same flexibility in solid1, we are to cover each call in an individual try-catch pair, making the coding style too heavy.

Whenever I return "useful" value from a method I regret it afterward. E.g.

set square=##class(mathCls).squareTriangle(a, b, c)

What if something went wrong, e.g. actual parameters (a, b, c) are invalid? I need some sign of it, so I'm forced to add some kind of status code. Let it be of %Status type. So, the caller's code is getting more complex:

set square=##class(mathCls).squareTriangle(a, b, c, .sc) if $$$ISERR(sc) $$$ThrowStatus(sc)

In contrast, when the status code is returned, it is not getting significantly complex:

$$$TOE(sc,##class(mathCls).squareTriangle(a, b, c, .square))

Besides, if each method returns status (even when it is always $$$OK), the whole code is looking more consistent and does not need modification if some of its methods get new behavior and able to return some error status as well.