Error Handling and Declaratives
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; an upcoming 4th edition is in ballot), where the conditional phrases and declaratives come from COBOL 85 and the exception conditions (EC-*) with RAISE, RESUME and the >>TURN directive were added by COBOL 2002; against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026) on z/OS 3.2; and against GnuCOBOL 3.2 (28 July 2023, the latest release), with which every program on it was compiled and run on 2026-10-20 with cobc -x -Wall (and again with -std=ibm, which printed the same output unless the text says otherwise). The programs that are deliberately wrong say so, and the messages shown are the ones cobc (GnuCOBOL) 3.2.0 and its run-time library printed. The programs that stop with a run-time error end with exit status 1; the exit status is quoted where it matters.
Error handling in COBOL is spread over four mechanisms that are easy to confuse. The conditional phrases (ON SIZE ERROR, AT END, INVALID KEY, and so on) are part of a single statement. The FILE STATUS clause leaves a two-character code after every file operation. Declaratives are sections that the run time performs when a file operation fails. Exception conditions are the standard’s named, checkable conditions (EC-SIZE-OVERFLOW, EC-BOUND-SUBSCRIPT and many more), which Enterprise COBOL does not implement as such but GnuCOBOL does, in part. A program ends by handing a condition code to the operating system through RETURN-CODE. The page covers them in that order and ends with what happens on z/OS when none of them catches the problem. The code examples use the Circulation public-library scenario: the member master MEMBMAST, the book master BOOKMAST and the loan transactions LOANTRAN. The mechanics of the file statements are on File Concepts: SELECT and FD and Sequential Files, and the complete list of status codes is on File Status Codes.
Where the book is outdated: Taulli’s Modern Mainframe Development (2022, chapter 5) introduces FILE STATUS as the way to learn whether a file operation worked, and that idea is still current. Two of its details are wrong and the correct rules are these: the status item is a separate two-character item in WORKING-STORAGE, not a field declared inside the record group (which would make it part of the data read from the file), and a file is opened with OPEN INPUT, not "OPEN READ". The book does not cover declaratives or the exception conditions at all, so everything from the declaratives section onward comes from the official documentation.
How an error travels through a COBOL program
When a file statement raises a condition, the standard and Enterprise COBOL take their actions in a fixed order, and GnuCOBOL 3.2 behaves the same way in every case tried here. The statement first stores the code in the FILE STATUS item, if the file has one. Then, if the condition is an AT END or INVALID KEY condition and the statement has the matching phrase, the phrase runs and the declarative is not executed. Otherwise an applicable USE AFTER ERROR declarative runs. If neither exists the outcome depends on the kind of error: a status-only error continues with the next statement when a FILE STATUS clause exists, and ends the run unit with a run-time error when none does.
The figure shows the order from the Language Reference rules for the invalid key condition and from the Programming Guide chapter on handling errors; the examples below exercise every branch.
Conditional phrases
A conditional statement is a statement that carries a phrase which depends on a condition that the statement itself detects. Every phrase comes in a pair: the positive one runs when the condition occurs, and the NOT form runs when the statement completes without it. Both contain imperative statements, and both are optional.
| Phrase pair | Statements | Condition detected | Notes |
|---|---|---|---|
|
|
The result does not fit the receiving item, or the divisor is zero; also the invalid exponentiations of the next section |
On a size error the receiving item keeps its old value; |
|
|
The pointer or the receiving item is too small for the operation |
The operation stops at once; with |
|
|
The called program cannot be loaded, the system cannot supply the input, and so on |
For |
|
|
No next record, no next sorted record, or no match in the table |
On a file it corresponds to status |
|
|
The key is missing, duplicated or out of sequence |
Not allowed on sequential files; status class |
|
|
The logical page is full |
Used by report programs |
Source: Enterprise COBOL for z/OS 6.5 Language Reference: Conditional statements; SIZE ERROR phrases; GnuCOBOL Programmer’s Guide: ON SIZE ERROR + NOT ON SIZE ERROR
A statement with one of these phrases is conditional and must be closed with its explicit scope terminator (END-ADD, END-READ, END-CALL, and so on) whenever it sits inside another statement, such as an IF branch. Without the terminator the next phrase or the period decides where the statement ends, which silently moves the following statements into the phrase. The scope terminators and the period rules are on Conditions: IF and EVALUATE. If a phrase’s imperative statement transfers control explicitly (a PERFORM of a paragraph that ends the program, for example), the rest of the statement is not executed.
Arithmetic: ON SIZE ERROR
A size error happens when the absolute value of the result, after decimal alignment, is larger than the receiving item can hold, or when a division by zero occurs. IBM adds three exponentiation cases: zero raised to the zero power (with the phrase the SIZE ERROR imperative runs; without it the value 1 is returned and a message is issued), zero raised to a negative number, and a negative number raised to a fractional power. The check applies only to final results, never to intermediate ones.
With the phrase coded and a size error present, the receiving item is not altered, the imperative statement runs, and the NOT ON SIZE ERROR branch is skipped. Without the phrase, the standard truncation rules apply, so the result is silently cut on the left, and a division by zero has no defined result. On z/OS, the fixed-point overflow and decimal overflow cases only reach the phrase when the TRAP(ON) run-time option is in effect, and a floating-point exponent overflow never causes a size error: an abend occurs instead.
The first program shows the phrases of MULTIPLY, DIVIDE, ADD, STRING and CALL in one place. The fine of 20000 days at 5.25 does not fit S9(5)V99, a divide by zero is a size error too, and the ADD without a phrase wraps around silently, which is the case to be afraid of.
IDENTIFICATION DIVISION.
PROGRAM-ID. PHRASES1.
* Conditional phrases on arithmetic, STRING and CALL.
DATA DIVISION.
WORKING-STORAGE SECTION.
COPY MEMBREC.
01 WS-FINE-DAYS PIC 9(5) VALUE 20000.
01 WS-FINE-RATE PIC 9(2)V99 VALUE 5.25.
01 WS-FINE-NEW PIC S9(5)V99 VALUE ZERO.
01 WS-FINE-EDIT PIC -(5)9.99.
01 WS-LABEL PIC X(20) VALUE SPACES.
01 WS-DIVISOR PIC 9(3) VALUE ZERO.
01 WS-QUOTIENT PIC 9(3) VALUE 999.
PROCEDURE DIVISION.
MAIN-PARA.
MOVE 101 TO MEMBER-ID
MOVE "Grace Hopper" TO MEMBER-NAME
MOVE 12.50 TO MEMBER-FINES
* ON SIZE ERROR: the result field is left unchanged.
MULTIPLY WS-FINE-DAYS BY WS-FINE-RATE GIVING WS-FINE-NEW
ON SIZE ERROR
DISPLAY "SIZE ERROR: fine not computed"
NOT ON SIZE ERROR
DISPLAY "fine computed"
END-MULTIPLY
MOVE WS-FINE-NEW TO WS-FINE-EDIT
DISPLAY "WS-FINE-NEW = " WS-FINE-EDIT
* Division by zero is also a size error.
DIVIDE MEMBER-FINES BY WS-DIVISOR GIVING WS-QUOTIENT
ON SIZE ERROR
DISPLAY "SIZE ERROR: divide by zero"
END-DIVIDE
DISPLAY "WS-QUOTIENT = " WS-QUOTIENT
* ADD without the phrase: the value is truncated silently.
ADD 99990.00 TO MEMBER-FINES
MOVE MEMBER-FINES TO WS-FINE-EDIT
DISPLAY "MEMBER-FINES = " WS-FINE-EDIT
* ON OVERFLOW on STRING: the receiving field is too short.
STRING FUNCTION TRIM(MEMBER-NAME) DELIMITED BY SIZE
" (member " DELIMITED BY SIZE
MEMBER-ID DELIMITED BY SIZE
")" DELIMITED BY SIZE
INTO WS-LABEL
ON OVERFLOW
DISPLAY "OVERFLOW: label truncated to [" WS-LABEL "]"
NOT ON OVERFLOW
DISPLAY "label complete"
END-STRING
* ON EXCEPTION on CALL: the called program cannot be loaded.
CALL "NOSUCHPG" USING MEMBER-ID
ON EXCEPTION
DISPLAY "EXCEPTION: NOSUCHPG not available"
END-CALL
STOP RUN.
SIZE ERROR: fine not computed
WS-FINE-NEW = 0.00
SIZE ERROR: divide by zero
WS-QUOTIENT = 999
MEMBER-FINES = 2.50
OVERFLOW: label truncated to [Grace Hopper (member]
EXCEPTION: NOSUCHPG not available
The WS-FINE-NEW and WS-QUOTIENT items kept the values they had before the failed statements, while the unprotected ADD turned 100002.50 into 2.50 without any message. The STRING stopped when the 20-character WS-LABEL was full, and the ON OVERFLOW branch ran.
Source: Enterprise COBOL for z/OS 6.5 Language Reference: SIZE ERROR phrases; STRING statement; Programming Guide: Handling errors in arithmetic operations
What an unprotected division by zero does is a difference between the compilers. This program is deliberately unprotected: it divides by a zero member count with neither ON SIZE ERROR nor any checking.
IDENTIFICATION DIVISION.
PROGRAM-ID. DIV0.
* Deliberately unprotected: division by zero without SIZE ERROR.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-FINES PIC 9(5)V99 VALUE 62.50.
01 WS-MEMBERS PIC 9(3) VALUE ZERO.
01 WS-AVERAGE PIC 9(5)V99 VALUE 777.
PROCEDURE DIVISION.
DIVIDE WS-FINES BY WS-MEMBERS GIVING WS-AVERAGE
DISPLAY "average " WS-AVERAGE
COMPUTE WS-AVERAGE = WS-FINES / WS-MEMBERS
DISPLAY "average " WS-AVERAGE
STOP RUN.
average 00777.00
average 00777.00
GnuCOBOL 3.2.0 carries on, leaves the result field alone and prints nothing (its manual says the run time prints an error message and aborts; the 3.2.0 binary used here did not, with or without -debug). Under -std=ibm the same program shows the value without a decimal point (0077700), because that dialect displays implied-decimal items as raw digits. On z/OS the same statement with a binary result is a fixed-point divide exception, which Language Environment reports as message CEE3209S with system completion code 0C9 and an abend, as the Programming Guide’s debugging chapter shows for a COMPUTE of a binary item by itself. This is z/OS only and was not run here. The portable rule is therefore to protect every division whose divisor can be zero, either with ON SIZE ERROR or with an explicit test before it.
STRING and UNSTRING: ON OVERFLOW
When the pointer of a STRING or UNSTRING falls outside the receiving field, COBOL does not let the overflow happen: the operation is not completed, the receiving field stays unchanged in the pointer case, and control passes to the next statement. If the program does not code ON OVERFLOW, nothing tells it that the operation was incomplete, which is the usual way truncated report lines are produced. Code the phrase on every STRING and UNSTRING whose result length is not obviously safe, and see Strings: STRING, UNSTRING and INSPECT for the statements themselves.
CALL: ON EXCEPTION and ON OVERFLOW
A dynamic CALL of a program that cannot be loaded (not found, or the system is out of storage) triggers ON EXCEPTION; without the phrase the application can abend on z/OS and stops with a run-time error in GnuCOBOL. The phrase applies only to the availability of the called program on its first load: if the program is loaded and then fails for another reason, such as its own initialization, the phrase does not run. ON OVERFLOW is a synonym of ON EXCEPTION on CALL. The usual pattern is to fall back to another program, as in the Programming Guide’s REPORTA/REPORTB fragment; subprogram calls are covered on Subprograms: CALL and LINKAGE.
This program is deliberately unprotected. It calls a program that does not exist, with no phrase:
IDENTIFICATION DIVISION.
PROGRAM-ID. NOCALL.
* Deliberately unprotected CALL of a missing program.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-ID PIC 9(6) VALUE 101.
PROCEDURE DIVISION.
CALL "NOSUCHPG" USING WS-ID
DISPLAY "not reached"
STOP RUN.
libcob: error: module 'NOSUCHPG' not found
The run-time library printed one line and the process ended with exit status 1; the DISPLAY after the CALL never ran.
One GnuCOBOL 3.2.0 behaviour needs care when both phrases are coded. The code that cobc generates tests the NOT ON EXCEPTION branch after the ON EXCEPTION branch has run, and a DISPLAY inside the first branch clears the exception that the second branch tests, so the NOT ON EXCEPTION statements ran as well. The arithmetic phrases are not affected (the same check on ADD ... ON SIZE ERROR ... NOT ON SIZE ERROR ran only the right branch). The portable form records the outcome in a flag and decides afterwards:
IDENTIFICATION DIVISION.
PROGRAM-ID. CALLQUIRK.
* NOT ON EXCEPTION after a DISPLAY inside ON EXCEPTION (GnuCOBOL).
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-MEMBER-ID PIC 9(6) VALUE 101.
01 WS-LOADED PIC X VALUE "Y".
88 PROGRAM-LOADED VALUE "Y".
88 PROGRAM-MISSING VALUE "N".
PROCEDURE DIVISION.
MAIN-PARA.
CALL "NOSUCHPG" USING WS-MEMBER-ID
ON EXCEPTION
DISPLAY "ON EXCEPTION ran"
NOT ON EXCEPTION
DISPLAY "NOT ON EXCEPTION ran"
END-CALL
* Portable form: record the outcome, decide afterwards.
SET PROGRAM-LOADED TO TRUE
CALL "NOSUCHPG" USING WS-MEMBER-ID
ON EXCEPTION
SET PROGRAM-MISSING TO TRUE
END-CALL
IF PROGRAM-MISSING
DISPLAY "NOSUCHPG is not available"
ELSE
DISPLAY "NOSUCHPG ran"
END-IF
STOP RUN.
ON EXCEPTION ran
NOT ON EXCEPTION ran
NOSUCHPG is not available
The standard and the Enterprise COBOL documentation describe only one of the two branches running; the second CALL above does what such a program is expected to do, and the first shows the 3.2.0 deviation. The run is the same under -std=ibm.
Files: AT END and INVALID KEY
AT END belongs to sequential reads: it runs when status 10 is set because no next record exists, and the NOT AT END branch runs only when the read succeeded. A read that fails for any other reason runs neither branch; control passes to the end of the statement after any declarative has run. INVALID KEY belongs to the keyed statements on indexed and relative files, and the NOT INVALID KEY branch runs when the key condition does not exist. The next program builds an indexed copy of BOOKMAST, reads the day’s LOANTRAN with AT END, and looks each book up with INVALID KEY. The file status items sit in WORKING-STORAGE, outside the record, as the errata above say.
IDENTIFICATION DIVISION.
PROGRAM-ID. FILES1.
* AT END, INVALID KEY and FILE STATUS on the Circulation files.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT LOANTRAN ASSIGN TO "LOANTRAN"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-TRAN-STATUS.
SELECT BOOKIDX ASSIGN TO "BOOKIDX.dat"
ORGANIZATION IS INDEXED
ACCESS MODE IS DYNAMIC
RECORD KEY IS BOOK-ID
FILE STATUS IS WS-BOOK-STATUS.
SELECT BOOKMAST ASSIGN TO "BOOKMAST"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-MAST-STATUS.
DATA DIVISION.
FILE SECTION.
FD LOANTRAN.
COPY LOANREC.
FD BOOKIDX.
COPY BOOKREC.
FD BOOKMAST.
01 BOOKMAST-RECORD PIC X(60).
WORKING-STORAGE SECTION.
01 WS-TRAN-STATUS PIC XX.
01 WS-BOOK-STATUS PIC XX.
01 WS-MAST-STATUS PIC XX.
01 WS-EOF PIC X VALUE "N".
88 END-OF-TRANS VALUE "Y".
01 WS-COUNT PIC 9(3) VALUE ZERO.
PROCEDURE DIVISION.
MAIN-PARA.
* Build an indexed copy of BOOKMAST.
OPEN INPUT BOOKMAST
OPEN OUTPUT BOOKIDX
PERFORM UNTIL WS-MAST-STATUS NOT = "00"
READ BOOKMAST
AT END
CONTINUE
NOT AT END
MOVE BOOKMAST-RECORD TO BOOK-RECORD
WRITE BOOK-RECORD
INVALID KEY
DISPLAY "duplicate book " BOOK-ID
END-WRITE
END-READ
END-PERFORM
CLOSE BOOKMAST BOOKIDX
OPEN INPUT LOANTRAN
OPEN INPUT BOOKIDX
PERFORM UNTIL END-OF-TRANS
READ LOANTRAN
AT END
SET END-OF-TRANS TO TRUE
NOT AT END
ADD 1 TO WS-COUNT
PERFORM CHECK-BOOK
END-READ
END-PERFORM
DISPLAY "transactions read: " WS-COUNT
" final status " WS-TRAN-STATUS
* A key that is not on the file
MOVE 999999 TO BOOK-ID
READ BOOKIDX
INVALID KEY
DISPLAY "book 999999 not found, status "
WS-BOOK-STATUS
NOT INVALID KEY
DISPLAY "book 999999 found"
END-READ
CLOSE LOANTRAN BOOKIDX
STOP RUN.
CHECK-BOOK.
MOVE LOAN-BOOK-ID TO BOOK-ID
READ BOOKIDX
INVALID KEY
DISPLAY "unknown book " LOAN-BOOK-ID
NOT INVALID KEY
DISPLAY "loan " LOAN-TYPE " book " BOOK-ID
" " BOOK-TITLE(1:20)
END-READ.
loan O book 000001 DATA STRUCTURES
loan O book 000002 COBOL BASICS
loan R book 000001 DATA STRUCTURES
loan O book 000003 BATCH PROCESSING
transactions read: 004 final status 10
book 999999 not found, status 23
The loop ended through AT END with the file status at 10, and the random read of a key that does not exist set 23. The invalid key condition of status 23 is recognized because the program coded the phrase; the declarative rules of the next section decide what happens when it does not.
Source: Enterprise COBOL for z/OS 6.5 Programming Guide: Coding INVALID KEY phrases; Programming Guide: Using the end-of-file condition (AT END); Language Reference: Common processing facilities
The two phrases are tied to the file organization, and both compilers enforce part of that. INVALID KEY on a sequential file is a compile-time error; this program is deliberately wrong:
IDENTIFICATION DIVISION.
PROGRAM-ID. WRONG1.
* Deliberately wrong: INVALID KEY on a sequential file.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT LOANTRAN ASSIGN TO "LOANTRAN"
ORGANIZATION IS LINE SEQUENTIAL.
DATA DIVISION.
FILE SECTION.
FD LOANTRAN.
COPY LOANREC.
PROCEDURE DIVISION.
OPEN INPUT LOANTRAN
READ LOANTRAN
INVALID KEY
DISPLAY "bad key"
END-READ
STOP RUN.
wrong1.cob:15: error: INVALID KEY clause invalid with this file type
13 | PROCEDURE DIVISION.
14 | OPEN INPUT LOANTRAN
15 > READ LOANTRAN
16 | INVALID KEY
17 | DISPLAY "bad key"
The reverse mistake is not caught at compile time by GnuCOBOL 3.2.0. AT END on a random read of an indexed file compiles without a warning, but a missing key is an invalid key condition, not the end of the file, so no phrase applies. The program is deliberately wrong:
IDENTIFICATION DIVISION.
PROGRAM-ID. WRONG2.
* Deliberately wrong: AT END on a random read of an indexed file.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT BOOKIDX ASSIGN TO "BOOKIDX.dat"
ORGANIZATION IS INDEXED
ACCESS MODE IS RANDOM
RECORD KEY IS BOOK-ID.
DATA DIVISION.
FILE SECTION.
FD BOOKIDX.
COPY BOOKREC.
PROCEDURE DIVISION.
OPEN INPUT BOOKIDX
MOVE 999999 TO BOOK-ID
READ BOOKIDX
AT END
DISPLAY "end of file"
END-READ
DISPLAY "after the READ"
CLOSE BOOKIDX
STOP RUN.
libcob: error: record key does not exist (status = 23) for file BOOKIDX ('BOOKIDX_dat' => BOOKIDX.dat)
libcob: warning: implicit CLOSE of BOOKIDX ('BOOKIDX.dat')
The run-time library reported status 23 as a fatal error and closed the files itself ("implicit CLOSE"), so after the READ was never displayed. An invalid key condition needs an INVALID KEY phrase, a declarative or at least a FILE STATUS clause to be survivable.
FILE STATUS in brief
After every file statement the two-character FILE STATUS item holds a code whose first digit gives the class: 0 successful, 1 at end, 2 invalid key, 3 permanent error, 4 logic error, and on z/OS 9 for IBM-defined conditions. The code is stored before any declarative, INVALID KEY phrase or AT END phrase runs, and it is not reset by the program: every statement sets it again. The rules that apply to every program are to give each file its own status item and to test it after each statement, or to rely on a declarative that does.
| Status | Meaning (standard and Enterprise COBOL) | Seen in this page |
|---|---|---|
|
Successful completion |
Every successful statement |
|
Sequential |
|
|
Random access to a record that does not exist |
|
|
|
The missing |
|
Sequential |
The read after the end of |
The full table, the z/OS 9x codes and the VSAM feedback codes are on File Status Codes.
Declaratives
A declarative is a section that the run time performs when an exceptional condition occurs, so that file errors are handled in one place and not after every statement. It is the oldest of COBOL’s handler mechanisms and still the only one that works in both Enterprise COBOL and GnuCOBOL.
Structure of the DECLARATIVES region
Declaratives sit at the very beginning of the PROCEDURE DIVISION, and as soon as there are any, the whole PROCEDURE DIVISION must be divided into sections. The rules from the Language Reference are:
-
The word
DECLARATIVESis written on the line after thePROCEDURE DIVISIONheader, andEND DECLARATIVEScloses the group. Each must begin in Area A, be followed by a separator period, and be alone on its line. -
Each declarative is a section whose header is followed by a period, and whose first sentence is a
USEstatement ending in a period. TheUSEstatement is never executed; it only tells the run time when to perform the paragraphs that follow it. -
Declarative sections take priority numbers from 0 through 49 if segmentation is used, which is rare.
-
A declarative procedure can be performed from an ordinary procedure and the reverse, and
GO TObetween them is allowed, but a declarative that runs a procedure that is already in control can loop forever. -
Execution of the program starts at the first statement after
END DECLARATIVES; declaratives never run by falling into them. Control returns to the I/O system when the last statement of the declarative has run, and the statement that failed then completes.
The full format of the statement is USE [GLOBAL] AFTER STANDARD {EXCEPTION | ERROR} PROCEDURE ON {file-name ... | INPUT | OUTPUT | I-O | EXTEND}. EXCEPTION and ERROR are synonyms, and GLOBAL makes the declarative available to the programs nested inside. With a file name, only that file’s errors are handled; with a mode, every file opened in that mode, or being opened in it, is handled. A file-specific declarative takes precedence over a mode declarative, and only one declarative may apply to a given file. GnuCOBOL’s manual adds that the words AFTER, FOR, ON, PROCEDURE and STANDARD are optional and that no two declaratives should be coded for the same event.
For nested programs the order of precedence is: a file-specific declarative in the program that contains the failing statement, then a mode-specific one in that program, then the same two with GLOBAL in the enclosing program, and so on outward until the outermost program. Only the first applicable declarative runs.
When the declarative runs
The error procedure is performed after the system’s own error routine has finished, when the failing statement is a READ, WRITE, REWRITE, START, OPEN, CLOSE or DELETE; when an AT END or INVALID KEY condition occurs and the statement has no matching phrase; and on the IBM-defined conditions that set file status key 1 to 9. If the file status does not indicate a critical error, the statement after the failed one runs when the declarative ends.
The following program has two declaratives: a file-specific one for the optional RENEWALS file and a mode-specific one for every file opened INPUT. The first OPEN fails with status 35. The member file is then read past its end four times with no AT END phrase, which makes the INPUT declarative run:
IDENTIFICATION DIVISION.
PROGRAM-ID. DECL1.
* A file-specific and an INPUT-mode declarative.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT RENEWALS ASSIGN TO "RENEWALS"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-RENEW-STATUS.
SELECT MEMBMAST ASSIGN TO "MEMBMAST"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-MEMB-STATUS.
DATA DIVISION.
FILE SECTION.
FD RENEWALS.
01 RENEWAL-RECORD PIC X(30).
FD MEMBMAST.
COPY MEMBREC.
WORKING-STORAGE SECTION.
01 WS-RENEW-STATUS PIC XX.
01 WS-MEMB-STATUS PIC XX.
01 WS-ERROR-COUNT PIC 9(2) VALUE ZERO.
PROCEDURE DIVISION.
DECLARATIVES.
RENEWALS-ERRORS SECTION.
USE AFTER STANDARD EXCEPTION PROCEDURE ON RENEWALS.
RENEWALS-REPORT.
ADD 1 TO WS-ERROR-COUNT
DISPLAY "declarative RENEWALS: status " WS-RENEW-STATUS.
INPUT-ERRORS SECTION.
USE AFTER STANDARD ERROR PROCEDURE ON INPUT.
INPUT-REPORT.
ADD 1 TO WS-ERROR-COUNT
DISPLAY "declarative INPUT: status " WS-MEMB-STATUS.
END DECLARATIVES.
MAIN-SECTION SECTION.
MAIN-PARA.
OPEN INPUT RENEWALS
IF WS-RENEW-STATUS NOT = "00"
DISPLAY "main: RENEWALS did not open"
END-IF
OPEN INPUT MEMBMAST
READ MEMBMAST
READ MEMBMAST
READ MEMBMAST
READ MEMBMAST
DISPLAY "main: last MEMBMAST status " WS-MEMB-STATUS
DISPLAY "declarative ran " WS-ERROR-COUNT " times"
CLOSE MEMBMAST
STOP RUN.
declarative RENEWALS: status 35
main: RENEWALS did not open
declarative INPUT: status 10
main: last MEMBMAST status 10
declarative ran 02 times
The RENEWALS declarative took precedence for its own file even though the file was opened INPUT, and the main line carried on after each error because the program has a FILE STATUS item. The fourth READ of the 3-member file ran the INPUT declarative with status 10 because there was no AT END phrase,, as the Programming Guide describes for a read without an AT END phrase.
A phrase wins over a declarative. This program has a declarative for BOOKIDX and a mix of statements with and without phrases; the declarative only runs where no phrase handles the condition:
IDENTIFICATION DIVISION.
PROGRAM-ID. ORDER1.
* Which handler runs: phrase, declarative, or both?
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT BOOKIDX ASSIGN TO "BOOKIDX.dat"
ORGANIZATION IS INDEXED
ACCESS MODE IS DYNAMIC
RECORD KEY IS BOOK-ID
FILE STATUS IS WS-BOOK-STATUS.
DATA DIVISION.
FILE SECTION.
FD BOOKIDX.
COPY BOOKREC.
WORKING-STORAGE SECTION.
01 WS-BOOK-STATUS PIC XX.
PROCEDURE DIVISION.
DECLARATIVES.
BOOK-ERRORS SECTION.
USE AFTER STANDARD ERROR PROCEDURE ON BOOKIDX.
BOOK-REPORT.
DISPLAY " declarative: status " WS-BOOK-STATUS.
END DECLARATIVES.
MAIN-SECTION SECTION.
MAIN-PARA.
OPEN INPUT BOOKIDX
MOVE 999999 TO BOOK-ID
DISPLAY "READ with INVALID KEY:"
READ BOOKIDX
INVALID KEY
DISPLAY " phrase: not found"
END-READ
DISPLAY "READ without a phrase:"
READ BOOKIDX
MOVE 1 TO BOOK-ID
START BOOKIDX KEY IS NOT LESS THAN BOOK-ID
PERFORM 4 TIMES
READ BOOKIDX NEXT
END-PERFORM
DISPLAY "fifth READ NEXT, with AT END:"
READ BOOKIDX NEXT
AT END
DISPLAY " phrase: end of file"
END-READ
DISPLAY "sixth READ NEXT, without AT END:"
READ BOOKIDX NEXT
CLOSE BOOKIDX
STOP RUN.
READ with INVALID KEY:
phrase: not found
READ without a phrase:
declarative: status 23
fifth READ NEXT, with AT END:
phrase: end of file
sixth READ NEXT, without AT END:
declarative: status 46
The INVALID KEY and AT END phrases ran and the declarative stayed quiet; the unprotected READ with no key found (status 23) and the sixth READ NEXT after the end of the file (status 46, a logic error, not an end-of-file condition) went to the declarative.
With and without a FILE STATUS clause
A declarative also keeps the program alive when the file has no FILE STATUS clause. Compare the same failing OPEN with no handler of any kind, and with only a declarative. The first program is deliberately unprotected:
IDENTIFICATION DIVISION.
PROGRAM-ID. NOHANDLER.
* Deliberately unprotected: no FILE STATUS, no declarative.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT RENEWALS ASSIGN TO "RENEWALS"
ORGANIZATION IS LINE SEQUENTIAL.
DATA DIVISION.
FILE SECTION.
FD RENEWALS.
01 RENEWAL-RECORD PIC X(30).
PROCEDURE DIVISION.
OPEN INPUT RENEWALS
DISPLAY "opened"
CLOSE RENEWALS
STOP RUN.
libcob: error: file does not exist (status = 35) for file RENEWALS ('RENEWALS')
That is the run-time error of the last box of the figure: the status was 35 and nothing handled it, so the run unit ended with exit status 1 and no further statement ran. With the declarative alone the program continues:
IDENTIFICATION DIVISION.
PROGRAM-ID. DECL2.
* Declarative but no FILE STATUS clause.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT RENEWALS ASSIGN TO "RENEWALS"
ORGANIZATION IS LINE SEQUENTIAL.
DATA DIVISION.
FILE SECTION.
FD RENEWALS.
01 RENEWAL-RECORD PIC X(30).
PROCEDURE DIVISION.
DECLARATIVES.
RENEWALS-ERRORS SECTION.
USE AFTER STANDARD ERROR PROCEDURE ON RENEWALS.
RENEWALS-REPORT.
DISPLAY "declarative: RENEWALS failed".
END DECLARATIVES.
MAIN-SECTION SECTION.
MAIN-PARA.
OPEN INPUT RENEWALS
DISPLAY "execution continues after the OPEN"
STOP RUN.
declarative: RENEWALS failed
execution continues after the OPEN
On z/OS the unprotected case is a severity-3 Language Environment condition that ends the run unit if it is not handled, and for serious QSAM errors without a status key or a declarative an abend results when the run-time option ABTERMENC(ABEND) is in effect, which is its IBM-supplied default. The Programming Guide adds a condition for QSAM files: if the program uses FILE STATUS or a declarative, the DD statement needs EROPT=ACC, otherwise the program cannot continue after some errors.
A few limits from the Language Reference matter for declaratives that do real work:
-
A declarative must not execute
GOBACKorSTOP RUNwhile it is active because of a QSAM abend on aREAD,WRITEorREWRITE(file status34or90), norEXIT PROGRAMin a non-nested subprogram; andGOBACKandEXIT PROGRAMare not allowed in a declarative active in a nested program or a method. The portable pattern, used by the batch program below, is to set a flag and a condition code in the declarative and to stop in the main line. -
In a multithreaded application, the lock of the file stays held while its declarative runs, so a declarative that reads another file whose declarative reads the first one can deadlock. Keep declaratives short and free of file operations.
-
A declarative for a file is executed in addition to the system’s standard routine, not instead of it.
Other USE formats
The USE statement has more formats than the error one:
-
USE BEFORE REPORTING identifier(Report Writer, COBOL 85; it runs just before a report group is presented, to adjust itsSUMandSOURCEitems or toSUPPRESSit) is the subject of Reports and Report Writer. -
USE AFTER EXCEPTION CONDITION exception-name(COBOL 2002) attaches a declarative to an exception condition and is described in the next section. -
USE FOR DEBUGGINGis an obsolete COBOL 85 debugging aid. It is mentioned only so that old code can be recognized: aUSE FOR DEBUGGING ONprocedure,ALL PROCEDURESorREFERENCES OFidentifier declarative is performed whenever that procedure runs or that item is referenced, and the program must sayWITH DEBUGGING MODEinSOURCE-COMPUTER. Enterprise COBOL 6.5 still accepts it (not in nested programs, methods, recursive programs or programs compiled withTHREAD) and supplies theDEBUG-ITEMspecial register. GnuCOBOL 3.2 parses it, but it takes effect only whenWITH DEBUGGING MODEis present at compile time and the environment variableCOB_SET_DEBUGis set toY,yor1at run time; in a test with those two switches on, the declarative ran after eachADDto the named item and a default build was silent, and under-std=ibmthe compiler warns that the clause is obsolete. New code should use a debugger,DISPLAYtracing under a compiler-directive switch, or the unit tests described on Testing COBOL Programs; the debugging tools are on Debugging and Troubleshooting.
Exception conditions (the EC names)
COBOL 2002 introduced a different idea: instead of each statement having its own phrase, the language defines a hierarchy of named exception conditions that the program can enable, test and handle uniformly. The top names are groups, the bottom names are specific. EC-ALL contains everything; EC-SIZE contains EC-SIZE-OVERFLOW, EC-SIZE-ZERO-DIVIDE, EC-SIZE-EXPONENTIATION and the other arithmetic size conditions; EC-BOUND contains EC-BOUND-SUBSCRIPT, EC-BOUND-REF-MOD and EC-BOUND-ODO; EC-I-O contains EC-I-O-AT-END, EC-I-O-INVALID-KEY, EC-I-O-PERMANENT-ERROR and the other file conditions; and there are groups for data (EC-DATA), arguments, program calls (EC-PROGRAM), ranges, storage, sort and merge, report, screen and others. In the standard each condition is either fatal, which ends the run unit when it is checked and occurs, or non-fatal, which only records the exception. The standard text itself is paywalled; the names and the fatal flag used here are those printed by cobc --list-exceptions and in Appendix F of the GnuCOBOL manual.
Compiler support is the real question
| Feature | Enterprise COBOL 6.5 | GnuCOBOL 3.2 |
|---|---|---|
|
None. A search of the 6.5 Language Reference and Programming Guide finds |
Supported. |
|
Not present in the 6.5 documentation |
Present; the manual says location and statement need |
|
Listed as potential reserved words only |
|
|
Not supported |
Parses, then warns "not implemented"; non-functional |
Run-time checking of subscripts and reference modification |
|
|
Run-time check of numeric data |
|
|
The practical consequence for portable programs is that EC-* is not portable between z/OS and GnuCOBOL: use it for GnuCOBOL development builds, and keep the production error handling in the mechanisms of the first half of the page. On Enterprise COBOL the equivalent run-time checks are compiler options: SSRANGE for subscripts, indexes and reference modification, NUMCHECK for invalid zoned and packed decimal data, and TRAP(ON) for arithmetic overflow; see Performance and Compiler Options.
The >>TURN directive and the cobc options
In GnuCOBOL the checks are generated into the program only when asked for. There are two ways to ask:
-
The compiler directive
>>TURN exception-name [file-name]... CHECKING ON [WITH LOCATION](orCHECKING OFF) switches the check on for the statements that follow it, down to a group name (EC-I-Oand a file name,EC-SIZE,EC-ALL). -
The command-line options
-fec=exception-name(and-fno-ec=exception-name) enable or disable the generation for a whole compilation;-debug(or-d) is-fstack-check -fec=EC-ALL. The manual says-fecalso turns on source-location tracking.cobc --list-exceptionsprints the names.
A checked exception is recorded in the run-time state and its name can be read with FUNCTION EXCEPTION-STATUS. For the exceptions that stop the program (a subscript or reference modification out of range, invalid numeric data, a program that cannot be found) the library prints a message with the source line and ends with exit status 1; the list marks 87 names as fatal with (f). The first program turns EC-SIZE checking on, adds to a field that cannot hold the result, and reads the recorded condition:
IDENTIFICATION DIVISION.
PROGRAM-ID. EC1.
* Exception checking with >>TURN and FUNCTION EXCEPTION-STATUS.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-FINE PIC 9(3) VALUE 990.
01 WS-STATUS PIC X(31).
01 WS-WHERE PIC X(40).
PROCEDURE DIVISION.
MAIN-PARA.
>>TURN EC-SIZE CHECKING ON
ADD 20 TO WS-FINE
MOVE FUNCTION EXCEPTION-STATUS TO WS-STATUS
DISPLAY "status: [" WS-STATUS "]"
DISPLAY "where : [" FUNCTION EXCEPTION-LOCATION "]"
DISPLAY "WS-FINE = " WS-FINE
STOP RUN.
status: [EC-SIZE-OVERFLOW ]
where : [EC1; MAIN-PARA; 12]
WS-FINE = 010
The unprotected ADD truncated 1010 to 010 as usual, and the exception was recorded as EC-SIZE-OVERFLOW, with EXCEPTION-LOCATION giving the program, paragraph and line. Note that EC-SIZE-OVERFLOW is marked fatal in the list but did not stop this program: the size family was only recorded, and execution continued in every test here, including EC-SIZE-ZERO-DIVIDE on a division without ON SIZE ERROR, so do not rely on the marker for the arithmetic conditions. The same program is accepted with -std=ibm and -std=cobol2014.
The file conditions are recorded in the same way, and three more functions describe them. EXCEPTION-FILE returns the two-digit file status immediately followed by the file name (34 characters in all, the name present only when the status is not 00), and EXCEPTION-STATEMENT names the failing verb:
IDENTIFICATION DIVISION.
PROGRAM-ID. EC4.
* EXCEPTION-* functions after file and arithmetic exceptions.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT RENEWALS ASSIGN TO "RENEWALS"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-RENEW-STATUS.
SELECT MEMBMAST ASSIGN TO "MEMBMAST"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-MEMB-STATUS.
DATA DIVISION.
FILE SECTION.
FD RENEWALS.
01 RENEWAL-RECORD PIC X(30).
FD MEMBMAST.
COPY MEMBREC.
WORKING-STORAGE SECTION.
01 WS-RENEW-STATUS PIC XX.
01 WS-MEMB-STATUS PIC XX.
01 WS-COUNT PIC 99 VALUE 99.
PROCEDURE DIVISION.
MAIN-PARA.
>>TURN EC-I-O CHECKING ON
>>TURN EC-SIZE CHECKING ON
OPEN INPUT RENEWALS
PERFORM SHOW-EXCEPTION
OPEN INPUT MEMBMAST
PERFORM 4 TIMES
READ MEMBMAST
END-PERFORM
PERFORM SHOW-EXCEPTION
ADD 1 TO WS-COUNT
PERFORM SHOW-EXCEPTION
CLOSE MEMBMAST
STOP RUN.
SHOW-EXCEPTION.
DISPLAY "status : " FUNCTION EXCEPTION-STATUS
DISPLAY "location : " FUNCTION EXCEPTION-LOCATION
DISPLAY "file : " FUNCTION EXCEPTION-FILE
DISPLAY "statement : " FUNCTION EXCEPTION-STATEMENT.
status : EC-I-O-PERMANENT-ERROR
location : EC4; MAIN-PARA; 27
file : 35RENEWALS
statement : OPEN
status : EC-I-O-AT-END
location : EC4; SHOW-EXCEPTION; 31
file : 10MEMBMAST
statement : READ
status : EC-SIZE-OVERFLOW
location : EC4; SHOW-EXCEPTION; 34
file : 00
statement : ADD
The OPEN of the missing file was recorded as EC-I-O-PERMANENT-ERROR (status 35), the READ past the end as EC-I-O-AT-END, and the ADD as a size overflow. The line number belongs to the statement that raised the condition (27, 31 and 34 in the listing), while the paragraph name shown is the one running when the function was called, which is why the second and third lines name SHOW-EXCEPTION.
A fatal exception: subscript out of range
A subscript beyond the table is the standard example of a fatal condition. With checking on, the program stops at the statement:
IDENTIFICATION DIVISION.
PROGRAM-ID. EC2.
* A fatal exception: subscript beyond the table.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 SHELF.
05 SLOT PIC X(10) OCCURS 3 TIMES.
01 WS-I PIC 9(2) VALUE 4.
PROCEDURE DIVISION.
MAIN-PARA.
>>TURN EC-BOUND-SUBSCRIPT CHECKING ON WITH LOCATION
MOVE "ULYSSES" TO SLOT (WS-I)
DISPLAY "not reached"
STOP RUN.
libcob: ec2.cob:12: error: subscript of 'SLOT' out of bounds: 4
note: maximum subscript for 'SLOT': 3
Last statement of "EC2" was MOVE
MAIN-PARA at ec2.cob:12
Started by ./ec2.out
The message names the source line, the table and the largest legal subscript, and the exit status is 1. The same program without checking is deliberately wrong and shows why the check exists:
IDENTIFICATION DIVISION.
PROGRAM-ID. EC3.
* Deliberately wrong: a subscript beyond the table, unchecked.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 SHELF.
05 SLOT PIC X(10) OCCURS 3 TIMES.
01 WS-I PIC 9(2) VALUE 4.
01 WS-AFTER PIC X(10) VALUE "UNTOUCHED".
PROCEDURE DIVISION.
MAIN-PARA.
MOVE "ULYSSES" TO SLOT (WS-I)
DISPLAY "WS-I = " WS-I
DISPLAY "WS-AFTER = [" WS-AFTER "]"
DISPLAY "RETURN-CODE = " RETURN-CODE
STOP RUN.
WS-I = 04
WS-AFTER = [UNTOUCHED ]
RETURN-CODE = +1163088729
Nothing stopped the move: SLOT (4) lies after the 30 bytes of SHELF, and in this build it landed on the storage of the RETURN-CODE special register. The register now holds 1163088729, whose low byte (89) became the process exit status (the run exited with 89). What gets overwritten depends on the build and the layout, but the lesson holds on every compiler: an unchecked subscript is a memory error, and in this case it also changed the program’s condition code. On z/OS the same program has no run-time check unless it is compiled with SSRANGE. The same effect is available without editing the source by compiling with -debug or -fec=bound-subscript; both stopped the program with the same message.
Other fatal conditions behave in the same way. With EC-DATA-INCOMPATIBLE on, an ADD of a zoned item holding 1A5 stops with 'WS-AMOUNT' (Type: NUMERIC DISPLAY) not numeric: '1A5', the GnuCOBOL counterpart of a data exception on z/OS, and with EC-BOUND-REF-MOD on, WS-TITLE (1:WS-LEN) with a length of 12 for a 10-byte item stops with length of 'WS-TITLE' out of bounds: 12, maximum: 10. Both programs exit with status 1.
Mistakes and the parts that are not implemented
An unknown exception name is a compile-time error, and RAISE and USE AFTER EXCEPTION CONDITION are accepted by the parser but do nothing. This program is deliberately wrong:
IDENTIFICATION DIVISION.
PROGRAM-ID. RAISE2.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-A PIC 9 VALUE 1.
PROCEDURE DIVISION.
MAIN-PARA.
>>TURN EC-SIZE CHECKING ON
>>TURN EC-NO-SUCH-THING CHECKING ON
RAISE EXCEPTION EC-USER
STOP RUN.
raise2.cob: in paragraph 'MAIN-PARA':
raise2.cob:9: error: invalid exception-name: EC-NO-SUCH-THING
7 | MAIN-PARA.
8 | >>TURN EC-SIZE CHECKING ON
9 > >>TURN EC-NO-SUCH-THING CHECKING ON
10 | RAISE EXCEPTION EC-USER
11 | STOP RUN.
raise2.cob:10: warning: RAISE statement is not implemented [-Wpending]
8 | >>TURN EC-SIZE CHECKING ON
9 | >>TURN EC-NO-SUCH-THING CHECKING ON
10 > RAISE EXCEPTION EC-USER
11 | STOP RUN.
12 | <EOF>
Compiled on its own, a program that declares USE AFTER EXCEPTION CONDITION EC-SIZE and executes RAISE EXCEPTION EC-USER compiles with two warnings and then runs, never calling the declarative. The program is deliberately wrong for GnuCOBOL 3.2:
IDENTIFICATION DIVISION.
PROGRAM-ID. RAISE1.
* Deliberately wrong for 3.2: RAISE and USE AFTER EXCEPTION
* CONDITION.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-A PIC 9 VALUE 1.
PROCEDURE DIVISION.
DECLARATIVES.
SIZE-HANDLER SECTION.
USE AFTER EXCEPTION CONDITION EC-SIZE.
SIZE-REPORT.
DISPLAY "size exception".
END DECLARATIVES.
MAIN-SECTION SECTION.
MAIN-PARA.
RAISE EXCEPTION EC-USER
STOP RUN.
raise1.cob: in section 'SIZE-HANDLER':
raise1.cob:11: warning: USE AFTER EXCEPTION CONDITION is not implemented [-Wpending]
9 | DECLARATIVES.
10 | SIZE-HANDLER SECTION.
11 > USE AFTER EXCEPTION CONDITION EC-SIZE.
12 | SIZE-REPORT.
13 | DISPLAY "size exception".
raise1.cob: in section 'MAIN-SECTION':
raise1.cob: in paragraph 'MAIN-PARA':
raise1.cob:17: warning: RAISE statement is not implemented [-Wpending]
15 | MAIN-SECTION SECTION.
16 | MAIN-PARA.
17 > RAISE EXCEPTION EC-USER
18 | STOP RUN.
19 | <EOF>
The GnuCOBOL manual says the same: the AFTER EXCEPTION CONDITION and AFTER EC clauses are recognized by the parser and do nothing. A program that depends on RAISE or on exception declaratives cannot work on either compiler today. The ON SIZE ERROR phrase of the first half of the page remains the way to handle a recorded size exception: with EC-SIZE-ZERO-DIVIDE checking on and the phrase coded, the phrase ran and FUNCTION EXCEPTION-STATUS still showed the condition.
IDENTIFICATION DIVISION.
PROGRAM-ID. DIV0EC2.
* With ON SIZE ERROR the checked division is handled, not fatal.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-FINES PIC 9(5)V99 VALUE 62.50.
01 WS-MEMBERS PIC 9(3) VALUE ZERO.
01 WS-AVERAGE PIC 9(5)V99 VALUE 777.
PROCEDURE DIVISION.
>>TURN EC-SIZE-ZERO-DIVIDE CHECKING ON WITH LOCATION
DIVIDE WS-FINES BY WS-MEMBERS GIVING WS-AVERAGE
ON SIZE ERROR
DISPLAY "no members: "
FUNCTION EXCEPTION-STATUS
END-DIVIDE
STOP RUN.
no members: EC-SIZE-ZERO-DIVIDE
COBOL 2023 also adds exception-related extensions (the outline mentions EC-I-O-WARNING and an inline exception-checking PERFORM); the names in cobc --list-exceptions for 3.2.0 include EC-I-O-RECORD-WARNING but no EC-I-O-WARNING, and neither compiler documents the inline form, so treat them as unavailable.
Run-time error procedures and Language Environment
When no phrase, status or declarative catches an error, the run-time system decides what happens, and each platform lets a program get control first.
GnuCOBOL. The routine CBL_ERROR_PROC registers one subprogram that the library calls when a run-time error is about to stop the program. In the test below it received the message text in its first parameter and could display the EXCEPTION-* functions (which need -debug, -g or -ftraceall for the location and statement) ; it cannot prevent the termination, because when the procedure returns the standard error routine runs. Only one procedure is in effect at a time. This program registers a procedure for the Circulation nightly run and then calls the missing program of the earlier example:
IDENTIFICATION DIVISION.
PROGRAM-ID. ERRPROC.
* A runtime error procedure registered with CBL_ERROR_PROC.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-HANDLER USAGE PROGRAM-POINTER.
01 WS-MEMBER-ID PIC 9(6) VALUE 101.
PROCEDURE DIVISION.
MAIN-PARA.
SET WS-HANDLER TO ENTRY "CIRCERR"
CALL "CBL_ERROR_PROC" USING 0, WS-HANDLER
DISPLAY "nightly run starting"
CALL "NOSUCHPG" USING WS-MEMBER-ID
DISPLAY "not reached"
STOP RUN.
END PROGRAM ERRPROC.
IDENTIFICATION DIVISION.
PROGRAM-ID. CIRCERR.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-TEXT PIC X(80).
01 WS-KIND PIC X(31).
LINKAGE SECTION.
01 LK-MESSAGE PIC X(80).
PROCEDURE DIVISION USING LK-MESSAGE.
MOVE FUNCTION EXCEPTION-STATUS TO WS-KIND
UNSTRING LK-MESSAGE DELIMITED BY LOW-VALUE INTO WS-TEXT
DISPLAY "CIRCERR: " FUNCTION TRIM(WS-TEXT)
DISPLAY "CIRCERR: status " FUNCTION TRIM(WS-KIND)
GOBACK.
END PROGRAM CIRCERR.
nightly run starting
CIRCERR: module 'NOSUCHPG' not found
CIRCERR: status EC-PROGRAM-NOT-FOUND
The handler read the message from its LINKAGE item and the condition name from EXCEPTION-STATUS before it did anything else, because the UNSTRING that cleans the message would overwrite the recorded exception (an earlier version of this program printed EC-OVERFLOW-UNSTRING). The process still ended with exit status 1. The message area ends in low-values rather than spaces, which is why the handler cuts it with UNSTRING before displaying it.
Enterprise COBOL on z/OS. A condition that is not handled is a Language Environment condition. COBOL can handle most errors with the phrases above, but for extraordinary conditions the program can register its own condition handler with the Language Environment callable service CEEHDLR, passing the entry address in a PROCEDURE-POINTER, and the handler can report, analyse, or even fix up a program and let it resume. The runtime messages are produced by signaling the matching condition, so a handler that does not deal with an information or warning level condition should percolate it. Two other callable services are used for diagnosis: CEE3DMP produces a formatted dump at a chosen point (symbolic names appear when the program is compiled with TEST and the dump uses the VARIABLES suboption), and CEE3ABD stops the run unit at once with an abend and a system dump when called with a cleanup value of zero. When the run unit ends abnormally with TRAP(ON), the run time closes the open files. If a handler reads or changes data defined in other programs (for example EXTERNAL items), the handler should declare them VOLATILE when the program is compiled with OPTIMIZE(1) or OPTIMIZE(2) and without TEST. How to read the resulting CEE messages, traceback and dump is on Debugging and Troubleshooting, and the run-time option defaults are on Compiling, Linking and Language Environment. The z/OS material here is documented behaviour that was not run.
RETURN-CODE, STOP RUN and the condition code
A COBOL program tells its caller how it went through the RETURN-CODE special register, implicitly defined as 01 RETURN-CODE GLOBAL PICTURE S9(4) USAGE BINARY VALUE ZERO in Enterprise COBOL (GnuCOBOL holds a larger binary value, as the earlier run showed). When the program ends, the value goes to the operating system as the user return code, or, when control returns to a calling program, to that program’s own RETURN-CODE. It is the batch condition code that JCL tests with COND or IF (see JCL for COBOL Programmers). A CALL ... RETURNING or a method does not set it, and Language Environment service calls do not return their feedback in it.
In Enterprise COBOL the STOP statement has two formats only: STOP RUN (the current RETURN-CODE, modulo 4096, becomes the return code; all files of the run unit are closed) and the obsolete STOP literal, which suspends execution until the operator acts and is better replaced by ACCEPT and DISPLAY. It has no status phrase. GOBACK is the usual way to leave a program that may be called or run on its own, and in a main program it acts like STOP RUN.
GnuCOBOL accepts more. In STOP RUN, RETURNING and GIVING are interchangeable and WITH ERROR STATUS and WITH NORMAL STATUS are alternatives, each optionally followed by a number (WITH and STATUS are optional words): without a number NORMAL gives 0 and ERROR gives 1. A program always returns a status, which is RETURN-CODE unless the statement says otherwise. The values returned by each form, measured on the Linux container used for this page, are:
MOVE 8 TO RETURN-CODE, then STOP RUN exit status 8
STOP RUN WITH NORMAL STATUS exit status 0
STOP RUN WITH ERROR STATUS exit status 1
STOP RUN WITH ERROR STATUS 12 exit status 12
STOP RUN RETURNING 16 exit status 16
MOVE 4 TO RETURN-CODE, then GOBACK exit status 4
MOVE 5000 TO RETURN-CODE, then STOP RUN exit status 136
MOVE -3 TO RETURN-CODE, then STOP RUN exit status 253
The last two lines show that a Unix process keeps only the low 8 bits of the value (5000 modulo 256, and -3 as 253), whereas z/OS takes the value modulo 4096; code that returns large condition codes must be written with the smaller range in mind. By convention a batch step returns 0 for success, 4 for warnings, 8 for errors, 12 for serious errors and 16 for severe errors, which is what the nightly validation program below uses.
STOP ERROR (GnuCOBOL 3.2). GnuCOBOL 3.2 added a STOP ERROR statement that stops the run with a run-time error. It belongs only to the GCOS dialect: it is rejected in the default dialect and in every other dialect tried, with the compile-time error "STOP ERROR does not conform to GnuCOBOL" (and the same wording naming IBM COBOL (lax), COBOL 2014 and the other dialects), and the dialect configuration files list stop-error-statement as ok only in gcos-strict. With -std=gcos the statement compiles, prints libcob: error: STOP ERROR when executed, and ends with exit status 1; it takes no value. STOP RUN WITH ERROR STATUS is the portable way to return a failure code.
Source: Enterprise COBOL for z/OS 6.5 Language Reference: STOP statement; RETURN-CODE special register; Programming Guide: Using the RETURN-CODE special register; GnuCOBOL Programmer’s Guide: STOP; GnuCOBOL 3.2 manual: dialect configuration
The Circulation nightly validation step
The last program puts the mechanisms together in the shape a z/OS batch step has. A declarative covers both input files, sets a flag and the severe condition code 16 and does nothing else, so the main line stays in charge of stopping; the members are loaded into a table, each loan transaction is checked with SEARCH ... AT END, a suspended member is rejected, and the step ends with 0 when all is well, 4 when any transaction was rejected and 16 when a file failed.
IDENTIFICATION DIVISION.
PROGRAM-ID. NIGHTRUN.
* Circulation nightly validation step: declaratives, FILE
* STATUS and a condition code in RETURN-CODE (0 clean, 4 rejects,
* 16 I/O error).
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT MEMBMAST ASSIGN TO "MEMBMAST"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-MEMB-STATUS.
SELECT LOANTRAN ASSIGN TO "LOANTRAN"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-TRAN-STATUS.
DATA DIVISION.
FILE SECTION.
FD MEMBMAST.
COPY MEMBREC.
FD LOANTRAN.
COPY LOANREC.
WORKING-STORAGE SECTION.
01 WS-MEMB-STATUS PIC XX.
01 WS-TRAN-STATUS PIC XX.
01 WS-FLAGS.
05 WS-IO-FAILED PIC X VALUE "N".
88 IO-FAILED VALUE "Y".
05 WS-TRAN-EOF PIC X VALUE "N".
88 TRAN-EOF VALUE "Y".
05 WS-MEMB-EOF PIC X VALUE "N".
88 MEMB-EOF VALUE "Y".
01 WS-COUNTS.
05 WS-MEMBER-COUNT PIC 9(3) VALUE ZERO.
05 WS-ACCEPTED PIC 9(3) VALUE ZERO.
05 WS-REJECTED PIC 9(3) VALUE ZERO.
01 MEMBER-TABLE.
05 MEMBER-ENTRY OCCURS 1 TO 50 TIMES
DEPENDING ON WS-MEMBER-COUNT
INDEXED BY MEMBER-IX.
10 TBL-MEMBER-ID PIC 9(6).
10 TBL-MEMBER-STATUS PIC X.
PROCEDURE DIVISION.
DECLARATIVES.
FILE-ERRORS SECTION.
USE AFTER STANDARD ERROR PROCEDURE ON MEMBMAST LOANTRAN.
FILE-ERRORS-REPORT.
SET IO-FAILED TO TRUE
MOVE 16 TO RETURN-CODE
DISPLAY "I/O error: MEMBMAST status " WS-MEMB-STATUS
", LOANTRAN status " WS-TRAN-STATUS.
END DECLARATIVES.
MAIN-SECTION SECTION.
MAIN-PARA.
PERFORM LOAD-MEMBERS
IF NOT IO-FAILED
PERFORM VALIDATE-LOANS
END-IF
DISPLAY "accepted " WS-ACCEPTED " rejected " WS-REJECTED
IF NOT IO-FAILED AND WS-REJECTED > ZERO
MOVE 4 TO RETURN-CODE
END-IF
DISPLAY "condition code " RETURN-CODE
STOP RUN.
LOAD-MEMBERS.
OPEN INPUT MEMBMAST
IF NOT IO-FAILED
PERFORM UNTIL MEMB-EOF OR IO-FAILED
READ MEMBMAST
AT END
SET MEMB-EOF TO TRUE
NOT AT END
ADD 1 TO WS-MEMBER-COUNT
MOVE MEMBER-ID
TO TBL-MEMBER-ID (WS-MEMBER-COUNT)
MOVE MEMBER-STATUS
TO TBL-MEMBER-STATUS (WS-MEMBER-COUNT)
END-READ
END-PERFORM
CLOSE MEMBMAST
END-IF.
VALIDATE-LOANS.
OPEN INPUT LOANTRAN
PERFORM UNTIL TRAN-EOF OR IO-FAILED
READ LOANTRAN
AT END
SET TRAN-EOF TO TRUE
NOT AT END
PERFORM CHECK-ONE-LOAN
END-READ
END-PERFORM
IF NOT IO-FAILED
CLOSE LOANTRAN
END-IF.
CHECK-ONE-LOAN.
SET MEMBER-IX TO 1
SEARCH MEMBER-ENTRY
AT END
ADD 1 TO WS-REJECTED
DISPLAY "reject: unknown member " LOAN-MEMBER-ID
WHEN TBL-MEMBER-ID (MEMBER-IX) = LOAN-MEMBER-ID
IF TBL-MEMBER-STATUS (MEMBER-IX) = "S"
ADD 1 TO WS-REJECTED
DISPLAY "reject: suspended member "
LOAN-MEMBER-ID
ELSE
ADD 1 TO WS-ACCEPTED
END-IF
END-SEARCH.
With the sample data (member 102, Alan Turing, is suspended) the run is clean except for one rejected checkout, so the condition code is 4:
reject: suspended member 000102
accepted 003 rejected 001
condition code +000000004
Pointing the LOANTRAN DD name at a file that is not there (in GnuCOBOL the DD_LOANTRAN environment variable overrides the file name, the equivalent of changing the DD statement) runs the same program into the declarative:
I/O error: MEMBMAST status 00, LOANTRAN status 35
accepted 000 rejected 000
condition code +000000016
exit=16
The first line comes from the declarative (the member master was fine, so its status is 00, and the transaction file reported 35), the main line skipped the validation, and the step ended with condition code 16, which is what a following JCL step would test. The default dialect displays RETURN-CODE with its sign (+000000016).
Standard, Enterprise COBOL and GnuCOBOL
| Feature | ISO/IEC 1989:2023 | Enterprise COBOL 6.5 | GnuCOBOL 3.2 |
|---|---|---|---|
Conditional phrases ( |
Yes (COBOL 85 and earlier) |
Yes |
Yes; |
|
Yes |
Yes, plus VSAM feedback and |
Yes |
|
Yes |
Yes, with |
Yes |
|
Not in COBOL 2002 or later |
Supported |
Parsed; needs |
|
COBOL 2002 onward |
Not implemented |
Names and |
Run-time checks |
|
|
|
|
Special register |
Yes, |
Yes, 32-bit; the OS keeps 8 bits on Unix |
|
Yes |
No |
Yes |
|
No |
No |
Only in |
Common mistakes
-
No
FILE STATUS, no phrase and no declarative on a file: the first failingOPENorREADends the run unit (GnuCOBOL:libcob: error: file does not exist (status = 35)and exit status 1). -
A
FILE STATUSitem declared inside the record: it becomes part of the data transferred; declare it inWORKING-STORAGE. -
INVALID KEYon a sequential file, orAT ENDfor a random read: the first is a compile-time error, the second compiles in GnuCOBOL and then fails at run time on a missing key. -
Testing the status only after the loop: the code is overwritten by every later statement on the file; test it right after the statement.
-
A
STOP RUNorGOBACKinside a declarative that was entered because of a QSAM abend: not allowed on z/OS; set a flag and stop in the main line. -
A declarative that reads a second file: it can deadlock in a multithreaded program and it obscures the original failure.
-
ADDorCOMPUTEwithoutON SIZE ERRORon a field that can overflow: the result is truncated silently (2.50 instead of 100002.50 above). -
A division without a zero test or
ON SIZE ERROR: an abend on z/OS and a silent no-op in GnuCOBOL 3.2.0. -
NOT ON EXCEPTIONafterDISPLAYinON EXCEPTION: the 3.2.0 deviation shown above; use a flag. -
Relying on
EC-*,RAISEorUSE AFTER EXCEPTION CONDITIONin code that must also run on z/OS: Enterprise COBOL has no equivalent; use the phrases,FILE STATUSand declaratives, andSSRANGEandNUMCHECK. -
Large
RETURN-CODEvalues: z/OS keeps 12 bits, Unix 8; keep to 0, 4, 8, 12 and 16. -
Reading the
EXCEPTION-*functions after another statement: any later statement, even anUNSTRINGor aDISPLAYof the functions, replaces the recorded exception; copy it first. -
Compiling production code with
-debug: it adds checks and slows the program; use it for tests and compile without it for the release.
References
-
Enterprise COBOL for z/OS 6.5 Language Reference: USE statement and EXCEPTION/ERROR declarative
-
Language Reference: Conditional statements and SIZE ERROR phrases
-
Language Reference: Common processing facilities (file status key and invalid key condition)
-
Language Reference: STOP statement and RETURN-CODE special register
-
Enterprise COBOL for z/OS 6.5 Programming Guide: Handling errors
-
GnuCOBOL Programmer’s Guide (DECLARATIVES, >>TURN, STOP, EXCEPTION-STATUS, ON SIZE ERROR, ON EXCEPTION, CBL_ERROR_PROC)
-
Tom Taulli, Modern Mainframe Development, O’Reilly Media, 2022 (chapter 5, consulted for the topics only)