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.

flowchart TD A["READ, WRITE, OPEN or another file statement"] --> B{"Condition raised?"} B -- "No" --> C["NOT AT END or NOT INVALID KEY phrase runs, if coded"] B -- "Yes" --> D["FILE STATUS item is set, if the file has one"] D --> E{"AT END or INVALID KEY condition with its phrase coded?"} E -- "Yes" --> F["The phrase runs and the declarative is skipped"] E -- "No" --> G{"A USE AFTER ERROR declarative applies?"} G -- "Yes" --> H["The declarative runs, then control returns after the statement"] G -- "No" --> I{"FILE STATUS coded?"} I -- "Yes" --> J["Execution continues and the program must test the status"] I -- "No" --> K["Run-time error: libcob message and exit 1 in GnuCOBOL, Language Environment condition on z/OS"]

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

ON SIZE ERROR / NOT ON SIZE ERROR

ADD, SUBTRACT, MULTIPLY, DIVIDE, COMPUTE

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; ROUNDED is applied before the check

ON OVERFLOW / NOT ON OVERFLOW

STRING, UNSTRING, and CALL (a synonym there for ON EXCEPTION)

The pointer or the receiving item is too small for the operation

The operation stops at once; with STRING the part already moved stays in the receiving item

ON EXCEPTION / NOT ON EXCEPTION

CALL, INVOKE, JSON and XML statements (Enterprise COBOL); ACCEPT and DISPLAY too in the standard and GnuCOBOL

The called program cannot be loaded, the system cannot supply the input, and so on

For CALL it covers only the first load of the program

AT END / NOT AT END

READ (sequential access), RETURN, SEARCH

No next record, no next sorted record, or no match in the table

On a file it corresponds to status 10

INVALID KEY / NOT INVALID KEY

READ, WRITE, REWRITE, DELETE, START on indexed and relative files

The key is missing, duplicated or out of sequence

Not allowed on sequential files; status class 2

AT END-OF-PAGE / NOT AT END-OF-PAGE

WRITE ... ADVANCING on a file with LINAGE

The logical page is full

Used by report programs

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.

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.

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

00

Successful completion

Every successful statement

10

Sequential READ found no next record (end of file)

transactions read: 004 final status 10

23

Random access to a record that does not exist

book 999999 not found, status 23

35

OPEN INPUT or I-O on a nonoptional file that is not available

The missing RENEWALS and LOANTRAN files below

46

Sequential READ with no valid next record, because the previous read failed or hit the end

The read after the end of BOOKIDX below

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 DECLARATIVES is written on the line after the PROCEDURE DIVISION header, and END DECLARATIVES closes 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 USE statement ending in a period. The USE statement 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 TO between 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 GOBACK or STOP RUN while it is active because of a QSAM abend on a READ, WRITE or REWRITE (file status 34 or 90), nor EXIT PROGRAM in a non-nested subprogram; and GOBACK and EXIT PROGRAM are 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 its SUM and SOURCE items or to SUPPRESS it) 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 DEBUGGING is an obsolete COBOL 85 debugging aid. It is mentioned only so that old code can be recognized: a USE FOR DEBUGGING ON procedure, ALL PROCEDURES or REFERENCES OF identifier declarative is performed whenever that procedure runs or that item is referenced, and the program must say WITH DEBUGGING MODE in SOURCE-COMPUTER. Enterprise COBOL 6.5 still accepts it (not in nested programs, methods, recursive programs or programs compiled with THREAD) and supplies the DEBUG-ITEM special register. GnuCOBOL 3.2 parses it, but it takes effect only when WITH DEBUGGING MODE is present at compile time and the environment variable COB_SET_DEBUG is set to Y, y or 1 at run time; in a test with those two switches on, the declarative ran after each ADD to the named item and a default build was silent, and under -std=ibm the compiler warns that the clause is obsolete. New code should use a debugger, DISPLAY tracing 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

EC-* exception names and the >>TURN directive to enable checking

None. A search of the 6.5 Language Reference and Programming Guide finds EC-SIZE-EXPONENTIATION only as a cross-reference in the Appendix I table of 2002/2014 features; there is no TURN directive.

Supported. cobc --list-exceptions prints 176 names, 87 of them marked fatal.

FUNCTION EXCEPTION-STATUS, EXCEPTION-LOCATION, EXCEPTION-FILE, EXCEPTION-STATEMENT

Not present in the 6.5 documentation

Present; the manual says location and statement need -debug, -ftraceall or -g (a >>TURN alone was enough in the tests below)

RAISE, RESUME

Listed as potential reserved words only

RAISE parses but is not implemented; RESUME is not a reserved word

USE AFTER EXCEPTION CONDITION

Not supported

Parses, then warns "not implemented"; non-functional

Run-time checking of subscripts and reference modification

SSRANGE compiler option (a different mechanism)

-debug or -fec=EC-BOUND-SUBSCRIPT, and so on

Run-time check of numeric data

NUMCHECK compiler option

-fec=EC-DATA-INCOMPATIBLE

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] (or CHECKING OFF) switches the check on for the statements that follow it, down to a group name (EC-I-O and 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 -fec also turns on source-location tracking. cobc --list-exceptions prints 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.

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 (SIZE ERROR, AT END, INVALID KEY, ON EXCEPTION)

Yes (COBOL 85 and earlier)

Yes

Yes; NOT ON EXCEPTION also runs after a DISPLAY in ON EXCEPTION on CALL

FILE STATUS

Yes

Yes, plus VSAM feedback and 9x codes

Yes

USE AFTER STANDARD ERROR/EXCEPTION PROCEDURE

Yes

Yes, with GLOBAL and the mode forms

Yes

USE FOR DEBUGGING

Not in COBOL 2002 or later

Supported

Parsed; needs WITH DEBUGGING MODE and COB_SET_DEBUG=Y

EC-* exceptions, >>TURN, RAISE, RESUME, USE AFTER EXCEPTION CONDITION

COBOL 2002 onward

Not implemented

Names and >>TURN yes; RAISE, USE AFTER EC accepted but not implemented

Run-time checks

>>TURN / exceptions

SSRANGE, NUMCHECK, TRAP

-debug, -fec=

RETURN-CODE

Special register

Yes, S9(4) binary, modulo 4096 at STOP RUN

Yes, 32-bit; the OS keeps 8 bits on Unix

STOP RUN WITH ERROR STATUS

Yes

No

Yes

STOP ERROR

No

No

Only in -std=gcos

Common mistakes

  • No FILE STATUS, no phrase and no declarative on a file: the first failing OPEN or READ ends the run unit (GnuCOBOL: libcob: error: file does not exist (status = 35) and exit status 1).

  • A FILE STATUS item declared inside the record: it becomes part of the data transferred; declare it in WORKING-STORAGE.

  • INVALID KEY on a sequential file, or AT END for 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 RUN or GOBACK inside 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.

  • ADD or COMPUTE without ON SIZE ERROR on 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 EXCEPTION after DISPLAY in ON EXCEPTION: the 3.2.0 deviation shown above; use a flag.

  • Relying on EC-*, RAISE or USE AFTER EXCEPTION CONDITION in code that must also run on z/OS: Enterprise COBOL has no equivalent; use the phrases, FILE STATUS and declaratives, and SSRANGE and NUMCHECK.

  • Large RETURN-CODE values: 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 an UNSTRING or a DISPLAY of 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.