The previous post covered RPG data structures in depth — dcl-ds syntax, qualified data structures with dot notation, LIKEDS for cloning a structure, template data structures, data structure arrays with DIM, external data structures from DB2 file definitions using EXTNAME and LIKEREC, nested data structures, and passing data structures to subprocedures. This post covers CL error handling and exception management: IBM i message types, the MONMSG command, program message queues, SNDPGMMSG, RCVMSG, condition handlers in CL procedures, DMPJOB for diagnostic dumps, and patterns for building robust CL programs that handle failures gracefully in production.
IBM i Message Types
Every error, warning, and status notification on IBM i travels through the message handling system — a structured mechanism for communicating between programs. Before writing error-handling code in CL, you must understand the six IBM i message types:
- Escape (*ESCAPE) — a fatal error. The message is sent to the call stack entry that called the failing program. If not monitored, it percolates up the call stack until it reaches an interactive job’s program message queue (causing the program to end with an error screen) or a batch job (causing the job to end abnormally). CPF and MCH message IDs are usually escape messages. For example, CPF0001 (Error found on command) is an escape message.
- Notify (*NOTIFY) — an error where the sending program expects a reply. Similar to escape but waits for a response from the caller before the sending program continues. Less common than escape messages.
- Status (*STATUS) — informational progress update; the caller can ignore it or monitor it. Often used by IBM i commands to report progress (e.g., SAVLIB sends status messages as objects are saved).
- Informational (*INFO) — informational message with no error implication. Sent to the job log or message queue for reference only.
- Completion (*COMP) — signals that a command or program completed successfully. Most IBM i commands send a completion message to the caller’s program message queue on success (e.g., CPFB861: Library MYLIB created).
- Inquiry (*INQ) — interactive; requires a human response. Only meaningful in interactive jobs. CL programs that send inquiry messages in batch jobs will cause the batch job to wait for a response indefinitely unless the INQMSGRPY job attribute is set.
Program Message Queues
Every program call stack entry on IBM i has its own program message queue. When a program sends a message to *PGMBDY, it goes to the caller’s program message queue. When a program sends to *EXT, it goes to the interactive user’s external message queue (the bottom line of a 5250 screen).
Message queues in the call stack:
*SAME— the current program’s own message queue*PGMBDY— the boundary program (the program that called the current procedure)*PRV— the next previous program entry in the call stack*EXT— the external (user-facing) message queue*SYSOPR— the system operator message queue (QSYSOPR)
MONMSG — Monitoring for Specific Exceptions
The MONMSG (Monitor Message) command is CL’s primary exception handler. It intercepts escape messages by message ID, preventing them from propagating up the call stack. MONMSG can be placed at the program level (monitoring all messages for the whole program) or immediately after a specific command (monitoring only that command).
/* Program-level MONMSG — catches any unhandled escape message
and jumps to the ERROR subroutine */
PGM
MONMSG MSGID(CPF0000 MCH0000) EXEC(GOTO CMDLBL(ERROR))
/* --- Normal processing starts here --- */
DLTF FILE(QTEMP/TMPFILE)
CPYF FROMFILE(APPLIB/CUSTMST) TOFILE(QTEMP/TMPFILE) +
MBROPT(*REPLACE) CRTFILE(*YES)
CALL PGM(APPLIB/PRCCUST)
GOTO CMDLBL(END)
ERROR:
/* Send the escape message to our caller so it knows we failed */
SNDPGMMSG MSG('CUSTJOB failed — see job log for details') +
MSGTYPE(*ESCAPE)
END:
ENDPGM
Command-level MONMSG — monitor a single command and continue if it fails:
PGM
/* Delete TMPFILE if it exists — ignore CPF2105 (object not found) */
DLTF FILE(QTEMP/TMPFILE)
MONMSG MSGID(CPF2105) /* CPF2105: File TMPFILE not found */
/* Create the file fresh */
CRTPF FILE(QTEMP/TMPFILE) RCDLEN(200)
/* Run a command and handle a specific error differently */
SBMJOB JOB(CUSTBATCH) JOBQ(APPLIB/APPJOBQ) +
CMD(CALL PGM(APPLIB/CUSTPRC))
MONMSG MSGID(CPF1318) EXEC(DO)
/* CPF1318: Job queue is held — send a notification */
SNDPGMMSG MSG('Job queue APPJOBQ is held — job not submitted') +
TOMSGQ(QSYSOPR)
GOTO CMDLBL(END)
ENDDO
END:
ENDPGM
Key MONMSG rules:
- MONMSG must immediately follow the command it monitors (no intervening commands)
- A program-level MONMSG (at the top of PGM block) catches all escape messages not caught by command-level MONMSG
- MONMSG only catches escape messages — not completion, informational, or status messages
- Monitoring
CPF0000catches all CPF messages;MCH0000catches all machine-level exceptions (overflow, divide by zero, etc.) - Monitoring
CPF9999alone only catches that one message — always useCPF0000for a general catch-all
SNDPGMMSG — Sending Messages from CL
CL programs send messages using SNDPGMMSG. This command is how you communicate status, errors, and completion information to callers and to the job log:
/* Send an informational message to the job log */
SNDPGMMSG MSG('Processing customer records...') +
MSGTYPE(*INFO)
/* Send a completion message to the caller */
SNDPGMMSG MSG('Customer batch completed. 1,247 records processed.') +
MSGTYPE(*COMP) +
TOPGMQ(*PRV)
/* Send an escape message to the caller — causes the caller to fail */
SNDPGMMSG MSG('Customer file locked — cannot continue') +
MSGTYPE(*ESCAPE) +
TOPGMQ(*PRV)
/* Send a message from a message file (for NLS and consistent messages) */
SNDPGMMSG MSGID(APP0001) MSGF(APPLIB/APPMSGF) +
MSGDTA('CUSTMST') +
TOPGMQ(*PRV) +
MSGTYPE(*ESCAPE)
/* Send a message to the system operator's message queue */
SNDPGMMSG MSG('Nightly batch started') +
TOMSGQ(QSYSOPR)
/* Send a message to a user's message queue */
SNDPGMMSG MSG('Your report is ready') +
TOMSGQ(DEVUSER)
RCVMSG — Retrieving Messages
RCVMSG retrieves a message from a program message queue. It is used when a CL program needs to capture the exact message text of an error that just occurred — for logging or forwarding:
PGM
DCL VAR(&MSGID) TYPE(*CHAR) LEN(7)
DCL VAR(&MSGTXT) TYPE(*CHAR) LEN(256)
DCL VAR(&MSGDTA) TYPE(*CHAR) LEN(100)
MONMSG MSGID(CPF0000 MCH0000) EXEC(GOTO CMDLBL(ERROR))
/* ... normal commands ... */
GOTO CMDLBL(END)
ERROR:
/* Retrieve the last escape message from our own message queue */
RCVMSG PGMQ(*SAME *) MSGTYPE(*LAST) +
MSGID(&MSGID) MSG(&MSGTXT) MSGDTA(&MSGDTA) +
RMV(*NO)
/* Log the message ID and text to a data queue or database */
CALL PGM(APPLIB/LOGERROR) PARM(&MSGID &MSGTXT)
/* Re-send as escape to notify our caller */
SNDPGMMSG MSGID(&MSGID) MSGF(QCPFMSG) +
MSGDTA(&MSGDTA) +
MSGTYPE(*ESCAPE) TOPGMQ(*PRV)
END:
ENDPGM
DMPJOB — Diagnostic Job Dump
DMPJOB (Dump Job) writes a detailed diagnostic dump of the current job to the spooled printer file QPSRVDMP. It is mainly a service tool for IBM support and is hard to read without internal knowledge. The dump includes:
- All program call stack entries with their local variable values
- Open data paths and file status
- Job attributes and environment at the time of the dump
- Data area contents
- Message queue contents
/* Dump job state from within a CL error handler — useful for debugging */
PGM
MONMSG MSGID(CPF0000 MCH0000) EXEC(GOTO CMDLBL(ERRHDL))
/* ... processing ... */
GOTO CMDLBL(END)
ERRHDL:
DMPJOB /* Spools the dump as QPSRVDMP */
/* Optionally also log to QHST or a custom log */
SNDPGMMSG MSG('Job dumped — see QPSRVDMP for details') +
TOMSGQ(QSYSOPR) MSGTYPE(*INFO)
SNDPGMMSG MSG('Fatal error in CUSTJOB') MSGTYPE(*ESCAPE) +
TOPGMQ(*PRV)
END:
ENDPGM
View the DMPJOB spooled file with: WRKJOB and its spooled files option, looking for the file named QPSRVDMP.
Scoped Error Handling with Subroutines and Called Programs
CL has no free-format procedure syntax: dcl-proc, monitor and exec sql belong to RPG, not CL. In CL, scope error handling with subroutines (SUBR, CALLSUBR, ENDSUBR), placed after the main logic and before ENDPGM, or by splitting work into separate called programs that each have their own MONMSG handling:
PGM
MONMSG MSGID(CPF0000 MCH0000) EXEC(GOTO CMDLBL(ERROR))
CALLSUBR SUBR(COPYCUST)
GOTO CMDLBL(END)
ERROR:
SNDPGMMSG MSG('CUSTJOB failed') TOPGMQ(*PRV) MSGTYPE(*ESCAPE)
END:
RETURN
SUBR SUBR(COPYCUST)
DLTF FILE(QTEMP/TMPCUST)
MONMSG MSGID(CPF2105) /* File not found: not an error */
CPYF FROMFILE(APPLIB/CUSTMST) TOFILE(QTEMP/TMPCUST) +
MBROPT(*REPLACE) CRTFILE(*YES)
ENDSUBR
ENDPGM
The command-level MONMSG inside the subroutine handles the expected failure locally; anything it does not handle reaches the program-level MONMSG. For record counts or other SQL, call an RPG or SQL-capable program, or use RUNSQL when no result is needed.
Production CL Error-Handling Template
A complete, production-ready CL program with full error handling, job log messaging, and QHST logging:
/* APPLIB/NIGHTBATCH — Nightly batch controller */
PGM
/* ── Declarations ── */
DCL VAR(&MSGID) TYPE(*CHAR) LEN(7)
DCL VAR(&MSGTXT) TYPE(*CHAR) LEN(256)
DCL VAR(&MSGDTA) TYPE(*CHAR) LEN(100)
DCL VAR(&STARTDT) TYPE(*CHAR) LEN(10)
DCL VAR(&STRTIM) TYPE(*CHAR) LEN(8)
/* ── Program-level catch-all ── */
MONMSG MSGID(CPF0000 MCH0000) EXEC(GOTO CMDLBL(ERRHDL))
/* ── Record start time ── */
RTVSYSVAL SYSVAL(QDATE) RTNVAR(&STARTDT)
RTVSYSVAL SYSVAL(QTIME) RTNVAR(&STRTIM)
SNDPGMMSG MSG('NIGHTBATCH started ' *CAT &STARTDT *BCAT &STRTIM) +
TOMSGQ(QSYSOPR) MSGTYPE(*INFO)
/* ── Step 1: Customer processing ── */
CALL PGM(APPLIB/CUSTPRC)
MONMSG MSGID(CPF0000 MCH0000) EXEC(DO)
SNDPGMMSG MSG('CUSTPRC failed — aborting NIGHTBATCH') +
TOMSGQ(QSYSOPR) MSGTYPE(*INFO)
GOTO CMDLBL(ERRHDL)
ENDDO
/* ── Step 2: Order processing ── */
CALL PGM(APPLIB/ORDPRC)
MONMSG MSGID(CPF0000 MCH0000) EXEC(DO)
SNDPGMMSG MSG('ORDPRC failed — aborting NIGHTBATCH') +
TOMSGQ(QSYSOPR) MSGTYPE(*INFO)
GOTO CMDLBL(ERRHDL)
ENDDO
/* ── Success ── */
SNDPGMMSG MSG('NIGHTBATCH completed successfully') +
TOMSGQ(QSYSOPR) MSGTYPE(*COMP)
GOTO CMDLBL(END)
/* ── Error handler ── */
ERRHDL:
RCVMSG PGMQ(*SAME *) MSGTYPE(*LAST) +
MSGID(&MSGID) MSG(&MSGTXT) MSGDTA(&MSGDTA) RMV(*NO)
DMPJOB
SNDPGMMSG MSG('NIGHTBATCH ABEND: ' *CAT &MSGID *BCAT &MSGTXT) +
TOMSGQ(QSYSOPR) MSGTYPE(*INFO)
CALL PGM(APPLIB/LOGERR) PARM(&MSGID &MSGTXT 'NIGHTBATCH')
SNDPGMMSG MSGID(&MSGID) MSGF(QCPFMSG) MSGDTA(&MSGDTA) +
MSGTYPE(*ESCAPE) TOPGMQ(*PRV)
END:
ENDPGM
This pattern — program-level MONMSG with a labelled error handler, RCVMSG to capture the failing message, DMPJOB for a diagnostic snapshot, SNDPGMMSG to QSYSOPR for operator visibility, and re-sending the original escape message to the caller — covers the requirements of most production CL batch jobs.
Next post: DB2 for i Query Optimization — SQE vs CQE query engines, Visual Explain in ACS, index design strategies for IBM i, encoded vector indexes (EVI), STRDBMON database monitoring, and reading the SQL plan cache using QSYS2 catalog views to diagnose slow queries.