This PowerUp blog entry was written by Isaac Ramírez, a software engineer at BAC Credomatic Network. Isaac is passionate about research and development in different technologies and languages on the IBM i platform.
A few years ago, when I’d just started learning RPG, I felt the language was missing something that I was more than used to in Java and .NET: exception handling. Sure RPG has the monitor instruction, but I was looking for a way to throw exceptions and retrieve aditional data about the exceptions. This was going to be especially useful when needed to investigate bugs in the production environment.
So, after looking around, I stumbled across an IBM Redpaper titled “RPG: Exception and Error Handling” (by Gary Mullen-Schutz, Susan Gantner, Jon Paris and Paul Tuohy). This Redpaper is a must read for all RPG Developers. It introduces the QMHSNDPM and QMHRCVPM APIs, which correspondingly, can be used to “throw” and “catch” exceptions. This gave me a nice way to start. I came to realize that I needed three functionalities:
- With a given message ID and message file, throw an exception that interrupts the current procedure and goes straight to the last on-error code in the call stack
- When an exception occurred, catch it and retrieve all of the related data
- Retrieve the call stack that caused the exception
Throwing Exceptions
With a few upgrades to the code included in the paper, I created the following procedure:
p CEXCEPTION_throwNewException...
p b export
d pi n
d msgID 10a const varying
d msgFile 10a const varying
d replaceVar 3000a const varying options(*nopass)
d
d errorApi ds likeds(errorDS_Type)
d qualifiedMsgFileName...
d s 20a
d msgKey s 4a
d replaceVarTemp s 3000a
/free
clear exception;
monitor;
if %parms() > 2;
replaceVarTemp = replaceVar;
endif;
qualifiedMsgFileName = msgFile;
%subst(qualifiedMsgFileName:11) = '*LIBL';
if CEXCEPTION_saveCallStack() and stackEntryArrayCount > 0;
CEXCEPTION_saveThrowInfo();
endif;
SendProgramMessage(msgId:
qualifiedMsgFileName:
replaceVarTemp:
%len(%trimr(replaceVarTemp)):
MESSAGE_TYPE_ESCAPE:
CALL_STACK_ENTRY:
CALL_STACK_COUNTER_THROW:
MsgKey:
errorApi);
on-error;
CEXCEPTION_jobPrintf('CEXCEPTION_throwNewException: +
Error throwing exception!');
return *off;
endmon;
/end-free
p CEXCEPTION_throwNewException...
p e
This procedure allows me to simulate the functionality of the throw instruction of the Java/C++ world. The following code shows how to use it:
select;
when sqlstt = '00000';
isFound = true;
when sqlstt = '02000';
isFound = false;
other;
CEXCEPTION_throwNewException('MSG0001':'CMSGFILE');
endsl;
Catching Exceptions
The next step in the process was implementing the catch funcionality. I wanted to be able to retrieve information like the procedure that caused the exception, the line number where the exception ocurred, the error code, the message file of the error code, the job and the datetime. Using the QMHRCVPM API, I created the following procedure:
p CEXCEPTION_catchException...
p b export
d pi n
d
d recoveredMessage...
d ds likeds(RCVM0300)
d messageKey s 4 inz(*ALLx'00')
d apiError ds likeds(errorDS_Type)
d
d ptrSenderInfo s * inz(*null)
d senderInfo ds likeds(RCVM0300SndRcvInfo)
d based(ptrSenderInfo)
/free
monitor;
ReceiveProgramMessage(recoveredMessage:
%size(recoveredMessage):
FORMAT_NAME:
CALL_STACK_ENTRY:
CALL_STACK_COUNTER:
MESSAGE_TYPE:
messageKey:
WAIT_TIME:
RECEIVE_ACTION:
apiError);
if recoveredMessage.ByteAvail > 0;
ptrSenderInfo = %addr(recoveredMessage.MsgData)
+ recoveredMessage.LenReplace1
+ recoveredMessage.LenMsgReturn
+ recoveredMessage.LenHelpReturn;
if %trimr(senderInfo.SendingProcedure) <> THROW_PROCEDURE_NAME;
clear exception;
CEXCEPTION_saveCallStack();
CEXCEPTION_saveSenderInfo(senderInfo);
endif;
exception.messageId = recoveredMessage.MsgId;
exception.messageFileName = recoveredMessage.MsgFileName;
exception.messageSeverity = recoveredMessage.MsgSeverity;
exception.messageFileLibrary = recoveredMessage.MsgLibUsed;
exception.messageText = %subst(recoveredMessage.MsgData:
recoveredMessage.LenReplace1 + 1:
recoveredMessage.LenMsgReturn);
exception.date = %date(%dec(senderInfo.DateSent:7:0):*cymd);
exception.time = %time(senderInfo.TimeSent:*hms0);
exception.jobName = psds.job_name;
exception.userProfile = psds.job_user;
exception.jobNumber = %char(psds.job_num);
if (recoveredMessage.LenReplace1 > 0);
exception.messageData = %subst(recoveredMessage.MsgData:1:
recoveredMessage.LenReplace1);
endif;
return *on;
With this procedure, all I had to do is call it right after the on-error instruction, in order to access all of the exception information:
monitor;
//Some code
on-error;
CEXCEPTION_catchException(); //must be the first instruction after the on-error
//some more core
endmon;
Saving the Call Stack
By this time, all that was left was a way to call the stack information for the most recent exception (i.e., get data related with all the procedures called before the exception ocurred). To my surprise, the QWVRCSTK API let me retrieve the call stack from a specific moment. So, I included procedures to save the stack trace before throwing and after catching exceptions. You can see this in the code for CEXCEPTION_throwNewException and CEXCEPTION_catchException with the call to the CEXCEPTION_saveStackTrace procedure. With this information available, I wrote a procedure to print the stack trace in the joblog and call it right after the CEXCEPTION_catchException in the on-error structure.
The following code shows how I implemented the CEXCEPTION_saveStackTrace procedure
monitor;
dou (CSTK0100.BytesRtn >= CSTK0100.BytesAvail);
GetCallStack(CSTK0100:
sizeCSTK0100:
CALL_STACK_INFO_FORMAT:
JIDF0100:
JOB_INFO_FORMAT:
errorApi);
if (sizeCSTK0100 < CSTK0100.BytesAvail);
sizeCSTK0100 = CSTK0100.BytesAvail;
ptrCSTK0100 = %realloc(ptrCSTK0100:sizeCSTK0100);
endif;
enddo;
ptrEntry = ptrCSTK0100 + CSTK0100.Offset;
if %parms > 2;
skipEntriesUsed = skipEntries;
endif;
for index = 1 to skipEntriesUsed;
ptrEntry += stackEntry.Len;
endfor;
if CSTK0100.Count > skipEntriesUsed;
for index = skipEntriesUsed + 1 to CSTK0100.Count;
clear newArrayEntry;
newArrayEntry.stackEntryInfo = stackEntry;
if stackEntry.StmtCnt > 0 and stackEntry.StmtDisp > 0;
pStatement = ptrEntry + stackEntry.StmtDisp;
newArrayEntry.statement = %triml(statements.identifier:'0');
endif;
if (stackEntry.ProcDisp > 0 and stackEntry.ProcLen > 0);
ptrName = ptrEntry + stackEntry.ProcDisp;
newArrayEntry.procedureName = %subst(procedureName:1:
stackEntry.ProcLen);
endif;
arraySize += 1;
entryArray(arraySize) = newArrayEntry;
ptrEntry = ptrEntry + stackEntry.Len;
if arraySize > ENTRY_ARRAY_SIZE;
leave;
endif;
endfor;
endif;
Source Code
At this time, all of the functionality shown is widely used in my company and has been very helpful to create programs that handle exceptions in a more “modern way.” Obviously, a lot more code is needed to have all the functionality described in this brief post. If you want more information or the source code, just contact me to the address isaac.ramirez.he[email protected]. I would appreciate your feedback in this topic.
You can even implement the "finally" clause in Java using API QMHSNDSM (Send Scope Message).
Too bad RPG doesn't support all this. Error handling is paramount with business apps.
Posted by: jacobus | March 21, 2012 at 11:06 AM
Thanks for the advice jacobus, I'll check it out later to improve it ;)
Posted by: Isaac Ramirez | March 21, 2012 at 12:31 PM
In the quest towards modern applications, decoupling your logic is key for achieving flexibility and maintainability.
This technique is much better than sending back an "error code" in a parameter and hopping that the calling program checks it after the call. I don't like to create components that need to be aware of who is going to use them or that deppend on external documentation, because I have found that it creates tight coupling.
This technique allows me to create ILERPG methods (aka.procedures) that would "throw" an exception messages so I can be sure that the calling program is aware of the exception and obligated to handle it accordingly.
I'm also signing @jacobus' wish list and hope to soon see this features as native functions of ILERPG.
Nice article!
Posted by: Jguty | March 21, 2012 at 03:13 PM
Oh but RPGLE (using ILE aren't you?) does support error handling much better than any other platform. Here's Why...
As a 40+ year veteran of the RPG and IBM i formerly known as System 3, System 36, System 38, AS/400...Been doing this for years.
Everything in the IBM i OS is in service programs. The RPG (ILE) programmer can use these same components by calling an API.
The API is the connection to the service programs that the IBM i uses to do just about everything.
You also need some error handling within each procedure that you create to call the API.
You do this by supplying an API Error structure as part of the API call to QWVRCSTK.
I am adding code below, but don't know how it will turn out, and it is only partial excerpts from the source members.
I can send the source files if you want.
PS.. Be better if this site had a way of attaching the code. It was pretty tedius pasting then editing the code entries. Still could not get them to show up, be my guest to edit the code sections.
The prototype to the API QWVRCSTK (this is in a separate source member and is copied in by the compiler thusley
D/include iAPISrvI,RtvCSkEV1P Retrieve call stack entry parm Ds and prototype
This is only the part that shows the prototype definition, there is much more in the source such as the formats and the detail data structure formats of the various types of data returned by the API.
D*** here is the prototype definition
D*------------------------------------
D iAPIRtvCallStackV1...
D PR Extpgm('QWVRCSTK')
D CStkReceiver...
D Like(CStk0100V1) //receiver variable
D CStkReceiverLength...
D Like(iAPI.ReceiverLength) //length receiver var
D CStkReceiverFormat...
D Like(iAPI.ReceiverFormat) //receiver var format
D CStKJobId...
D Like(JIDF0100V1) //Job identifier info
D CStkJobIdFormat...
D Like(iAPI.JobIdFormat) //Job ID format name
D CStkAPIErrorData...
D Like(ErrC0200V1) //API error return
D*------------------------------------
here is what the call looks like
// retrieve the call stack
iAPIRtvCallStackV1
( CStk0100V1 // call stack receiver structure:
xnReceiverLength // length of receiver (Stk0100V1):
xcReceiverFormat // format name of receiver structure:
JIDF0100V1 // job ID information structure;
xcJobIdFormat // format name for the Job ID structure:
ErrC0200V1 ); // API errors structure
Here is what that error structure looks like
D*------------------------------------
------------------------------------------------
D ErrC0200V1...
D DS Qualified
D Key...
D 9B 0 key indicates format
D BytesProvided...
D Like(iAPI.BytesProvided) bytes provided here
D BytesAvailable...
D Like(iAPI.BytesAvailable) rtrn bytes available
D MessageID...
D Like(Gr.MessageID) message identifier
D Reserved...
D 1A error reserved byte
D CCSId...
D 9B 0 CCSID exception data
D*------------------------------------
------------------------------------------------
There are status codes named constants that are returned to the caller of the API
blank - information complete
I - information retrieved but incomplete
N - no information could be retrieved
the simple test to see if an API error occurred is coded in a subroutine (include source member) like this
//========================================================
Begsr ErrCV10200APIErrorTest;
If ErrC0200V1.BytesAvailable > *Zeros; // some error information was returned
iAPIResult = iAPI_Result_Error; // errors occurred
Else;
iAPIResult = iAPI_Result_Successful; // no errors, API call was successful
EndIf;
//========================================================
The iAPI_Result_Error and the _Successful are named constants (from included source)
I have tons more of the API processing code (procedures and service programs).
Plus groupings of the procedures in service programs.
I have a service program just for the error handling. Remember we want to only have one set of code to maintain.
Here is an export from a service program that componentizes (did i spell that right) the API calls
StrPgmExp
PgmLvl(*Current) +
LvlChk(*Yes) +
Signature('V1_2010.04.01')
/* ('1234567890123456') */
Export Symbol(ChgLibListToJobDInlLibLV1)
Export Symbol(CrtUserSpaceV1)
Export Symbol(LstDataBaseMembersV1)
Export Symbol(LstDataBaseRelationsMemberV1
Export Symbol(RtvLibListFromSupGroupsV1)
Export Symbol(RtvNextDataBaseMemberV1)
Export Symbol(RtvNextDataBaseRelationsMemberV1)
Export Symbol(RtvCallStackEntryV1)
Export Symbol(RtvDbFileOverrideV1)
Export Symbol(RtvJobDescriptionV1)
Export Symbol(RtvObjectDescriptionV1)
Export Symbol(RtvUserInformationV1)
Export Symbol(RtvSystemValueV1
)
EndPgmExp
Each of the above uses and IBM i API to do the job.
Posted by: Thomas Rock | March 23, 2012 at 10:43 AM
Oops, I forgot to mention the monitor only is good for catching errors that you might expect to happen. But what about the errors that are not included in the monitor.
Try coding a subroutine called *PSSR. This is automatically run if an error occurrs that is not handled by a monotor. This has been the standard of error handling in RPG for oh I don't know how many years ago that was.
Posted by: Thomas Rock | March 23, 2012 at 10:48 AM
Hi Thomas...Thanks for the feedback, I really appreciate it. Can you please send me a copy of your code? I think that creating an API that covers the things that you point out in your comment and merging it with what I already have, will result in a very robust solution.
Sorry for the inconveniences with the source code, this is my first post, so I didn´t know the way this site will handle it...
Posted by: Isaac | March 24, 2012 at 05:52 PM
Thomas:
Oh but RPGLE (using ILE aren't you?) does support error handling much better than any other platform
You mean i/OS, the operating system, not RPG, the language.
Indeed, i/OS is very robust, and provides a native exception handling mechanism lightyears ahead of what you have in in Unix, Win etc.
Too bad RPG doesn't support this, although it is meant for building robust business applications. You have to implement the solution yourself.
Too bad.
In the "other" world it's the other way around. A rather crude and primitive OS (compared with i/OS), but advanced languages with proper support for error handling.
Posted by: jacobus | March 28, 2012 at 10:13 AM
Here is an example using a utility
http://www.perficient.com/Solutions-and-Services/Business-Integration-SOA/Reusable-Utility-Services
Posted by: Eric Roch | May 08, 2012 at 12:26 PM