Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

COBOL  Module 3 – Numbers in storage, money arithmetic and dates

Invalid numeric data, 0C7 abends and validation

What COBOL does with spaces or bad sign half-bytes in numeric fields: the NUMERIC test, GnuCOBOL run-time checks, z/OS 0C7 data exceptions and reject files.

  • Intermediate
  • 15 minutes
  • Examples run with GnuCOBOL 3.2 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Validate input with the NUMERIC class test before using it in arithmetic
  • Explain how invalid zoned or packed data behaves in GnuCOBOL with and without run-time checks
  • Match a z/OS 0C7 data exception and the NUMPROC and NUMCHECK options to GnuCOBOL's behaviour
  • Route bad records to an error file instead of ending the run

Before you start

What you’ll learn · Before you start

What you will learn

  • Validate input with the NUMERIC class test before using it in arithmetic
  • Explain how invalid zoned or packed data behaves in GnuCOBOL with and without run-time checks
  • Match a z/OS 0C7 data exception and the NUMPROC and NUMCHECK options to GnuCOBOL's behaviour
  • Route bad records to an error file instead of ending the run

Before you start

On this page

A record whose amount field holds five spaces goes through ADD in a GnuCOBOL build without checks as if the amount were 0, and the run’s total is wrong by that record without a message. On z/OS, invalid digits or signs in decimal data are the classic cause of an S0C7 abend, which stops the job instead.

Below you will test fields with IS NUMERIC before any arithmetic, see what GnuCOBOL does with bad zoned and packed bytes with and without run-time checks, match that to the 0C7 data exception and to IBM’s NUMPROC and NUMCHECK options, and route bad records to a reject file with a reason while the run goes on.

IS NUMERIC tests the bytes against the PICTURE and the USAGE

A PICTURE describes what an item should hold, and nothing checks it when a record is read or a group is moved. The class condition: A test of which characters an item holds: NUMERIC for digits only, ALPHABETIC for letters and spaces, and ALPHABETIC-UPPER or ALPHABETIC-LOWER for one case of letters and spaces. More in “Conditions, IF and EVALUATE decision tables” IS NUMERIC is the check: IBM’s guide says it tests the contents of an item “against a set of values that are valid for the PICTURE and USAGE”. For zoned decimal that means a digit in every byte, with a valid sign in the last zone of a signed item; for packed decimal, digits 0 to 9 in every digit half-byte and a valid sign in the last. This program sets three zoned values and six packed ones through a REDEFINES, as bad data arrives in a file, and tests each:

classtest.cob: three zoned and six packed values tested COB · classtest.cob
       IDENTIFICATION DIVISION.
       PROGRAM-ID. CLASSTEST.
      * The NUMERIC class test on three zoned fields and six packed
      * ones. Each field's bytes are set through a REDEFINES, as bad
      * data arrives from a file, and printed in hexadecimal.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  Z-AMT        PIC 9(5).
       01  Z-TEXT       REDEFINES Z-AMT PIC X(5).
       01  P-RAW        PIC X(3).
       01  P-AMT        REDEFINES P-RAW PIC S9(5) COMP-3.
       PROCEDURE DIVISION.
           DISPLAY "PIC 9(5)"
           MOVE "01234" TO Z-TEXT
           PERFORM TEST-ZONED
           MOVE SPACES TO Z-TEXT
           PERFORM TEST-ZONED
           MOVE "1234X" TO Z-TEXT
           PERFORM TEST-ZONED
           DISPLAY "PIC S9(5) COMP-3"
           MOVE X'12345C' TO P-RAW
           PERFORM TEST-PACKED
           MOVE X'12345D' TO P-RAW
           PERFORM TEST-PACKED
           MOVE X'12345F' TO P-RAW
           PERFORM TEST-PACKED
           MOVE X'12345A' TO P-RAW
           PERFORM TEST-PACKED
           MOVE X'1234AC' TO P-RAW
           PERFORM TEST-PACKED
           MOVE X'404040' TO P-RAW
           PERFORM TEST-PACKED
           STOP RUN.
       TEST-ZONED.
           IF Z-AMT IS NUMERIC
               DISPLAY "  " FUNCTION HEX-OF(Z-TEXT) "  numeric"
           ELSE
               DISPLAY "  " FUNCTION HEX-OF(Z-TEXT) "  not numeric"
           END-IF.
       TEST-PACKED.
           IF P-AMT IS NUMERIC
               DISPLAY "  " FUNCTION HEX-OF(P-RAW) "      numeric"
           ELSE
               DISPLAY "  " FUNCTION HEX-OF(P-RAW) "      not numeric"
           END-IF.
IS NUMERIC in two dialects Python · classtest_session.py
from session import files, run

# The NUMERIC class test, built with GnuCOBOL's default dialect and with -std=ibm. -A passes an option on to the C
# compiler: this one keeps it quiet about the byte X'AC' inside a literal.
files("classtest.cob")
run("""
cobc -x -A -Wno-invalid-source-encoding classtest.cob
./classtest
cobc -x -std=ibm -A -Wno-invalid-source-encoding -o classtest-ibm classtest.cob
./classtest-ibm
""")

Output

$ cobc -x -A -Wno-invalid-source-encoding classtest.cob
$ ./classtest
PIC 9(5)
  3031323334  numeric
  2020202020  not numeric
  3132333458  not numeric
PIC S9(5) COMP-3
  12345C      numeric
  12345D      numeric
  12345F      not numeric
  12345A      not numeric
  1234AC      not numeric
  404040      not numeric
$ cobc -x -std=ibm -A -Wno-invalid-source-encoding -o classtest-ibm classtest.cob
$ ./classtest-ibm
PIC 9(5)
  3031323334  numeric
  2020202020  not numeric
  3132333458  not numeric
PIC S9(5) COMP-3
  12345C      numeric
  12345D      numeric
  12345F      numeric
  12345A      not numeric
  1234AC      not numeric
  404040      not numeric

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 classtest_session.py

The zoned field passed only with five digits: spaces, X’2020202020’, and 1234X, whose last byte is X’58’, both failed. The packed field passed with X’12345C’ and X’12345D’. It failed with X’1234AC’, whose fifth digit half-byte is A, with X’12345A’, and with X’404040’, the bytes of three EBCDIC spaces, whose sign half-byte is 0. Step through the packed test:

Interactive · stage by stage IS NUMERIC on a packed field, half-byte by half-byte Step down the column to see which half-bytes the test reads and why two of the six packed values failed it.
IS NUMERIC on a packed field, half-byte by half-byte, step 5 of 5. Showing Five digit half-bytes, X'1234AC': A is no digit, One sign half-byte, X'12345F': sign F, -std=ibm accepts F. In focus: -std=ibm accepts F.Five digit half-byteseach must be 0 to 9X'1234AC': A is no digitnot numeric in every buildOne sign half-byteC or D for a signed itemX'12345F': sign Fnot numeric by default-std=ibm accepts Fas IBM does: C, D or F

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The digits. PIC S9(5) COMP-3 is three bytes, five digit half-bytes and a sign half-byte. The class test reads the digit half-bytes first, and each must hold 0 to 9.
  2. A bad digit. X'1234AC' has the half-byte A where the fifth digit should be. A is 10, no decimal digit, so the field is not numeric in both builds, whatever its sign says.
  3. The sign. The last half-byte must be a sign the test accepts. X'12345C' and X'12345D', positive and negative, passed in both builds.
  4. An F sign. X'12345F' ends in F, the sign of an unsigned item. For this signed item GnuCOBOL's default dialect said not numeric, and X'12345A' failed too.
  5. The IBM dialect. Built with -std=ibm, whose configuration sets hostsign to yes, the same X'12345F' was numeric. IBM's guide gives C, D and F as the valid signs of a signed item under NUMPROC(NOPFD), the default, with NUMCLS(PRIM).

X’12345F’ is the case that differs by dialect. F is the sign of an unsigned item, and GnuCOBOL’s default dialect rejected it in a signed field, while -std=ibm, whose configuration sets hostsign to yes, accepted it. IBM’s guide gives the same answer for z/OS: with NUMCLS(PRIM) in effect the valid signs of a signed item under NUMPROC(NOPFD) are C, D and F.

Testing the amount after the arithmetic

Wrong: ADD REC-AMT TO TOTAL, then IF REC-AMT IS NUMERIC to report the record. The bad bytes are already in the total. Right: test where the record comes in, before any statement uses the field as a number, and keep the record out of every total when it fails.

Placed there, the test turns a bad field into a decision the program makes, instead of a number it computes.

Key idea

IS NUMERIC checks the bytes, not the value: digits in every position and a sign the dialect accepts, so a field should pass it before any arithmetic reads it.

Checkpoint

Check: The NUMERIC class test

  1. Question 1 of 2 A PIC 9(5) field holds five spaces. What does IF AMT IS NUMERIC decide?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It is not numeric

    The class test checks every byte against the PICTURE and the USAGE, so spaces, letters and stray signs all fail.

  2. Question 2 of 2 Which of these PIC S9(5) COMP-3 values passed IS NUMERIC in both of the lesson's builds?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: X'12345D'

    Digit half-bytes must be 0 to 9 and the sign half-byte a sign the dialect accepts: C and D always, F under the IBM rules.

Without checks GnuCOBOL computes with bad bytes; a debug build stops

What happens when a program skips the test depends on how it was built. This program adds an amount of spaces, the amount 1234X and the packed bytes X’1234AC’ to totals. The session builds it three times: with GnuCOBOL’s defaults, with -debug, which turns on every run-time check, and with -fec=EC-DATA-INCOMPATIBLE, which turns on only the check for numeric data that is not numeric:

bad-data.cob: arithmetic on three bad amounts COB · bad-data.cob
       IDENTIFICATION DIVISION.
       PROGRAM-ID. BAD-DATA.
      * Arithmetic on invalid numeric data, as a record read from a
      * file might hold it: an amount of spaces, an amount ending in a
      * letter, and a packed field with a hexadecimal A among its
      * digits.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  IN-REC.
           05  REC-ID     PIC X(4).
           05  REC-AMT    PIC 9(5).
       01  P-RAW          PIC X(3).
       01  P-AMT          REDEFINES P-RAW PIC S9(5) COMP-3.
       01  TOTAL          PIC 9(7) VALUE ZERO.
       01  P-TOTAL        PIC S9(7) COMP-3 VALUE ZERO.
       01  E-OUT          PIC -(7)9.
       PROCEDURE DIVISION.
           MOVE "A001     " TO IN-REC
           ADD REC-AMT TO TOTAL
           DISPLAY "after an amount of spaces  TOTAL " TOTAL
           MOVE "A0021234X" TO IN-REC
           ADD REC-AMT TO TOTAL
           DISPLAY "after the amount 1234X     TOTAL " TOTAL
           MOVE X'1234AC' TO P-RAW
           ADD P-AMT TO P-TOTAL
           MOVE P-TOTAL TO E-OUT
           DISPLAY "after packed X'1234AC'     TOTAL " E-OUT
           STOP RUN.
Three builds of the same program Python · bad_data_session.py
from session import files, run

# The same program built three ways: with no run-time checks, with -debug, and with only the check for incompatible
# data. The line "Started by" names the program the shell ran.
files("bad-data.cob")
run("""
cobc -x -A -Wno-invalid-source-encoding bad-data.cob
./bad-data
cobc -x -debug -A -Wno-invalid-source-encoding -o bad-data-debug bad-data.cob
!./bad-data-debug
cobc -x -fec=EC-DATA-INCOMPATIBLE -A -Wno-invalid-source-encoding -o bad-data-ec bad-data.cob
!./bad-data-ec
""")

Output

$ cobc -x -A -Wno-invalid-source-encoding bad-data.cob
$ ./bad-data
after an amount of spaces  TOTAL 0000000
after the amount 1234X     TOTAL 0012348
after packed X'1234AC'     TOTAL    12350
$ cobc -x -debug -A -Wno-invalid-source-encoding -o bad-data-debug bad-data.cob
$ ./bad-data-debug
libcob: bad-data.cob:19: error: 'REC-AMT' (Type: NUMERIC DISPLAY) not numeric: '     '

 Last statement of "BAD-DATA" was ADD at line 19 of bad-data.cob
	ENTRY BAD-DATA at bad-data.cob:18
 Started by ./bad-data-debug
[exit status 1]
$ cobc -x -fec=EC-DATA-INCOMPATIBLE -A -Wno-invalid-source-encoding -o bad-data-ec bad-data.cob
$ ./bad-data-ec
libcob: bad-data.cob:19: error: 'REC-AMT' (Type: NUMERIC DISPLAY) not numeric: '     '

 Last statement of "BAD-DATA" was ADD at line 19 of bad-data.cob
 Started by ./bad-data-ec
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bad_data_session.py

The default build added all three without a word. The total stayed 0000000 after the spaces, became 0012348 after 1234X, and the packed total became 12350. GnuCOBOL 3.2’s NEWS explains the first two: for invalid numeric DISPLAY data it now uses “only the second half-byte” of each character, so a space, X’20’, is 0 and the X, X’58’, is 8, and it warns that results differ from earlier versions. The packed half-byte A was read as 10. Both checked builds stopped at the first ADD instead, with the field, its type and its bytes, ‘REC-AMT’ (Type: NUMERIC DISPLAY) not numeric, the statement, ADD at line 19, and exit status 1. Step through the two outcomes:

Interactive · step by step ADD of an amount that holds spaces, in two builds Step through one ADD in a build without checks, on the left, and with -debug, on the right.
ADD of an amount that holds spaces, in two builds, step 6 of 6. Showing REC-AMT holds 5 spaces, ADD REC-AMT TO TOTAL, Default: no check, -debug: test first, TOTAL stays 0000000, Stops at line 19. In focus: Stops at line 19.REC-AMT holds 5 spacesX'2020202020'ADD REC-AMT TO TOTALline 19Default: no checkeach space read as 0-debug: test firstEC-DATA-INCOMPATIBLETOTAL stays 0000000the run goes onStops at line 19exit status 1

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The record. The amount field of the record holds five spaces, X'2020202020', as a short or empty line of a file leaves it. PIC 9(5) promises digits, and nothing checked the promise when the record was read.
  2. The ADD. Line 19 adds REC-AMT to the total. What happens next depends on how the program was built, not on the source.
  3. No checks. The default build adds without testing. GnuCOBOL 3.2 reads only the second half-byte of each invalid character, so every space, X'20', counts as the digit 0.
  4. A silent zero. TOTAL stays 0000000 and the run carries on. The amount 1234X, next in the run, was taken as 12348 the same way, its X'58' read as an 8.
  5. With -debug. Built with -debug, or with only -fec=EC-DATA-INCOMPATIBLE, the program tests the field before it uses it, and finds it not numeric.
  6. A stop with its reason. libcob names the field, its type and its bytes, 'REC-AMT' (Type: NUMERIC DISPLAY) not numeric, and the last statement, ADD at line 19, then ends the run with exit status 1.

Run tests and new feeds with the data check on

Build test runs, and the first runs of a new input feed, with -fec=EC-DATA-INCOMPATIBLE. A stop that names the field and the statement costs minutes; a total built from bytes read as zeros can cost a reconciliation.

The two outcomes come from the same source and differ only in how it was built, so the build options belong in a program’s documentation, next to its record layouts.

Key idea

Without checks GnuCOBOL turns bad bytes into plausible numbers, and a build with -debug or -fec=EC-DATA-INCOMPATIBLE stops at the first one, with the field and the statement named.

Checkpoint

Check: Invalid data with and without run-time checks

  1. Question 1 of 2 In the build without checks, a TOTAL of zero had the zoned amount 1234X added to it. What did TOTAL hold?

    Type a number.

    A hint

    GnuCOBOL 3.2 reads only the second half-byte of an invalid character; X is X'58'.

    Show the answer: Question 1 of 2

    Answer: 12348

    The X counted as its second half-byte, 8, so 1234X was taken as 12348 and the run printed 0012348.

  2. Question 2 of 2 What did the -debug build do at the ADD of an amount holding spaces?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It stopped, named the field and the statement, and ended with exit status 1

    -debug turns on every run-time check, including EC-DATA-INCOMPATIBLE, which -fec can turn on alone.

On z/OS the same bytes end the step with a data exception

The mainframe’s decimal arithmetic does not carry on with bad bytes. IBM’s MVS system codes list 0C7 as a data exception: The z/OS program check of system completion code 0C7, raised when decimal arithmetic meets a zoned or packed operand with invalid digits or an invalid sign. It ends the step unless the program prevents it., and IBM’s page on the INVDATA option describes the data behind it, DISPLAY and packed items “that could contain invalid digits, an invalid sign code, or invalid zone bits”, and the S0C7 abend an invalid sign can cause. A job that meets a packed field of EBCDIC spaces, X’404040’, stops there, where the GnuCOBOL build without checks carried on. Three compiler options decide what Enterprise COBOL accepts and checks:

Option and default What it does Nearest in GnuCOBOL
NUMPROC, default NOPFD accepts any valid sign; PFD is faster but needs the preferred signs C, D and F -fhostsign, set by -std=ibm, for the sign F
NUMCHECK, default NONUMCHECK adds numeric class tests to zoned and packed sending items; MSG warns, ABD ends the run -fec=EC-DATA-INCOMPATIBLE, which ends the run
INVDATA, default NOINVDATA tells the compiler that DISPLAY and packed data may be invalid, to stay close to older compilers none

IBM warns that NUMCHECK is “much slower” than NONUMCHECK, so it suits test runs and the first runs of a new feed better than everyday production. The local relative of a mainframe dump is -fdump=ALL, which prints the program’s data after the stop:

The checked build with -fdump=ALL Python · dump_session.py
from session import files, run

# The -debug build again, with -fdump=ALL: after the error, libcob prints the program's data. The dump's header names
# the time the program was compiled, which changes with every build, so the session shows it as "(build time)".
files("bad-data.cob")
run("""
cobc -x -debug -fdump=ALL -A -Wno-invalid-source-encoding -o bad-data-dump bad-data.cob
!./bad-data-dump
""", mask=[(r"compiled [A-Z][a-z]{2} +\d{1,2} \d{4} \d{2}:\d{2}:\d{2}", "compiled (build time)")])

Output

$ cobc -x -debug -fdump=ALL -A -Wno-invalid-source-encoding -o bad-data-dump bad-data.cob
$ ./bad-data-dump
libcob: bad-data.cob:19: error: 'REC-AMT' (Type: NUMERIC DISPLAY) not numeric: '     '

 Last statement of "BAD-DATA" was ADD at line 19 of bad-data.cob
	ENTRY BAD-DATA at bad-data.cob:18
 Started by ./bad-data-dump

Dump Program-Id BAD-DATA from bad-data.cob compiled (build time)

WORKING-STORAGE
**********************
77        RETURN-CODE                     +000000000
01        IN-REC.
  05      REC-ID                         'A001'
  05      REC-AMT
01        P-RAW                          ALL SPACES
01        TOTAL                           0000000
01        P-TOTAL                         +0000000
01        E-OUT                                  0

END OF DUMP - BAD-DATA
**********************
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dump_session.py

The dump shows the record as it arrived, REC-ID ‘A001’ and nothing but blanks after REC-AMT, next to the totals, which is the first thing to read after an abend: the field that holds the bad bytes, and the record it came from.

Bad input is also an attack surface

Treat every input record and screen field as untrusted, as OWASP’s Input Validation Cheat Sheet advises for data that crosses a trust boundary: check its type, format and range where it enters, with an allowlist of the values the program accepts.

Checkpoint

Check: Data exceptions, NUMPROC and NUMCHECK on z/OS

  1. One question Which system completion code is a data exception on z/OS?

    Choose one answer.

    Show the answer: One question

    Answer: 0C7

    IBM's INVDATA option page describes such data, invalid digits, sign codes or zone bits in DISPLAY and packed items, and the S0C7 abend that an invalid sign can cause.

Validate every record where it enters, and route the bad ones

Stopping at the first bad record is right in a test and expensive in a nightly batch, where one bad amount would hold back thousands of good ones. The pattern that keeps both is to validate each record before any arithmetic, write the good ones on and the bad ones to a reject file: A file to which a program writes the input records it refuses, each with a reason code and its bytes as they arrived, so the run can go on and the records can be corrected and sent again. with a reason code, and report the count to the caller. This program does it for eight synthetic transactions; the file statements are the subject of a later lesson, and only the checks matter here:

validate.cob: four checks and two output files COB · validate.cob
       IDENTIFICATION DIVISION.
       PROGRAM-ID. VALIDATE.
      * Reads transactions, checks each one before any arithmetic, and
      * writes it either to the accepted file or, with a reason code,
      * to the reject file. Ends with RETURN-CODE 4 when it rejected
      * any record.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT TX-FILE ASSIGN TO "transactions.txt"
               ORGANIZATION IS LINE SEQUENTIAL
               FILE STATUS IS TX-STATUS.
           SELECT OK-FILE ASSIGN TO "accepted.txt"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT BAD-FILE ASSIGN TO "rejects.txt"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  TX-FILE.
       01  TX-REC.
           05  TX-ACCOUNT     PIC X(6).
           05  TX-CODE        PIC X.
               88  TX-CODE-OK VALUE "D" "W".
           05  TX-AMOUNT      PIC 9(5)V99.
       FD  OK-FILE.
       01  OK-REC             PIC X(14).
       FD  BAD-FILE.
       01  BAD-REC.
           05  BAD-REASON     PIC XX.
           05  FILLER         PIC X.
           05  BAD-DATA       PIC X(14).
           05  FILLER         PIC X.
           05  BAD-TEXT       PIC X(24).
       WORKING-STORAGE SECTION.
       01  TX-STATUS          PIC XX.
       01  END-OF-FILE        PIC X VALUE "N".
           88  NO-MORE        VALUE "Y".
       01  REASON             PIC XX.
           88  RECORD-OK      VALUE SPACES.
       01  REASON-TEXT        PIC X(24).
       01  MAX-AMOUNT         PIC 9(5)V99 VALUE 50000.00.
       01  READ-COUNT         PIC 9(5) VALUE ZERO.
       01  OK-COUNT           PIC 9(5) VALUE ZERO.
       01  BAD-COUNT          PIC 9(5) VALUE ZERO.
       01  OK-TOTAL           PIC 9(7)V99 VALUE ZERO.
       01  E-TOTAL            PIC Z,ZZZ,ZZ9.99.
       PROCEDURE DIVISION.
           OPEN INPUT TX-FILE
           IF TX-STATUS NOT = "00"
               DISPLAY "cannot open transactions.txt: " TX-STATUS
               MOVE 16 TO RETURN-CODE
               STOP RUN
           END-IF
           OPEN OUTPUT OK-FILE BAD-FILE
           PERFORM UNTIL NO-MORE
               READ TX-FILE
                   AT END SET NO-MORE TO TRUE
                   NOT AT END PERFORM CHECK-AND-ROUTE
               END-READ
           END-PERFORM
           CLOSE TX-FILE OK-FILE BAD-FILE
           MOVE OK-TOTAL TO E-TOTAL
           DISPLAY "read " READ-COUNT ", accepted " OK-COUNT
               ", rejected " BAD-COUNT ", accepted total " E-TOTAL
           IF BAD-COUNT > ZERO
               MOVE 4 TO RETURN-CODE
           END-IF
           STOP RUN.
       CHECK-AND-ROUTE.
           ADD 1 TO READ-COUNT
           MOVE SPACES TO REASON REASON-TEXT
           EVALUATE TRUE
               WHEN TX-ACCOUNT = SPACES
                   MOVE "04" TO REASON
                   MOVE "no account number" TO REASON-TEXT
               WHEN NOT TX-CODE-OK
                   MOVE "03" TO REASON
                   MOVE "unknown transaction code" TO REASON-TEXT
               WHEN TX-AMOUNT IS NOT NUMERIC
                   MOVE "01" TO REASON
                   MOVE "amount is not numeric" TO REASON-TEXT
               WHEN TX-AMOUNT = ZERO OR TX-AMOUNT > MAX-AMOUNT
                   MOVE "02" TO REASON
                   MOVE "amount out of range" TO REASON-TEXT
           END-EVALUATE
           IF RECORD-OK
               ADD 1 TO OK-COUNT
               ADD TX-AMOUNT TO OK-TOTAL
               WRITE OK-REC FROM TX-REC
           ELSE
               ADD 1 TO BAD-COUNT
               MOVE SPACES TO BAD-REC
               MOVE REASON TO BAD-REASON
               MOVE TX-REC TO BAD-DATA
               MOVE REASON-TEXT TO BAD-TEXT
               WRITE BAD-REC
           END-IF.
Eight transactions in, two files out Python · validate_session.py
from session import files, run

# Eight synthetic transactions through the validation program, then the two files it wrote. The program ends with
# RETURN-CODE 4 because it rejected records, so the shell reports exit status 4.
files("validate.cob", "transactions.txt")
run("""
cat transactions.txt
cobc -x validate.cob
!./validate
cat accepted.txt
cat rejects.txt
""")

Output

$ cat transactions.txt
A10001D0250000
A10002W00500
A10003D0000000
A10004X0010000
A10005W9999999
      D0001000
A10007W0012550
A10008D00-1500
$ cobc -x validate.cob
$ ./validate
read 00008, accepted 00002, rejected 00006, accepted total     2,625.50
[exit status 4]
$ cat accepted.txt
A10001D0250000
A10007W0012550
$ cat rejects.txt
01 A10002W00500   amount is not numeric
02 A10003D0000000 amount out of range
03 A10004X0010000 unknown transaction code
02 A10005W9999999 amount out of range
04       D0001000 no account number
01 A10008D00-1500 amount is not numeric

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 validate_session.py

Two records were accepted, for a total of 2,625.50, and six went to rejects.txt, each with its reason and its bytes as they arrived. The run went to its end, and RETURN-CODE 4 told the shell that something was refused. The checks run in order, from the bytes to the meaning: an account is present, the code is one the program knows, the amount is numeric, and only then is the amount compared with the business range, because a comparison on bad bytes is exactly what the class test prevents. Step through one record:

Interactive · step by step One transaction through the four checks Step through the checks in the order the program makes them. A record that fails one goes right, to the reject file, with the reason on the arrow.
One transaction through the four checks, step 7 of 7. Showing Read the record, Account present?, Code D or W?, Amount IS NUMERIC?, Amount in range?, Write to accepted.txt, Write to rejects.txt. In focus: Write to rejects.txt.04030102Read the record8 records in the fileAccount present?Code D or W?Amount IS NUMERIC?Amount in range?Write to accepted.txt2 recordsWrite to rejects.txt6 records, with reasons

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Read. READ brings one transaction of 14 bytes into TX-REC, an account, a code and an amount. Nothing about the bytes is checked yet.
  2. The account. The first WHEN of the EVALUATE tests for an account of spaces. The sixth record failed here and went to the reject file with reason 04.
  3. The code. The condition name TX-CODE-OK lists the codes the program knows, D and W, an allowlist. The fourth record, code X, failed with reason 03.
  4. The class test. Only now does the program look at the amount, with IS NUMERIC before any arithmetic. The amounts 00500 with two spaces and 00-1500 failed with reason 01.
  5. The range. A numeric amount can still be wrong for the business. Zero and 99999.99, above the limit of 50000.00, failed with reason 02.
  6. Accepted. A record that passed all four checks is added to the total and written to accepted.txt. Two did, for a total of 2,625.50.
  7. Rejected. Each failed record went to rejects.txt with its reason code and its bytes as they arrived. Six did, the run went on to the end, and RETURN-CODE 4 told the caller.

The code check is an allowlist: the condition name TX-CODE-OK lists D and W, so X, and any code nobody thought of, is refused. OWASP’s cheat sheet gives the same advice: state the values a program accepts, and refuse everything outside them.

Key idea

Validate every record where it enters, from the bytes to the business rules, and route a failure to a reject file with a reason, so the good records still run and the bad ones can be corrected.

Checkpoint

Check: Validating and routing bad records

  1. One question In validate.cob, which reason code did the record A10008D00-1500 get?

    Choose one answer.

    Show the answer: One question

    Answer: 01, amount is not numeric

    The EVALUATE takes the first check that fails, in the order account, code, numeric, range.

Practise: find the bad bytes before the program does

Each question shows bytes or a record from this lesson’s runs. Decide what the test, the build or the check does with them before you choose:

Practice that adapts to you

Practise this lesson

12 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What does the NUMERIC class test check?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: That the item's bytes are valid for its PICTURE and USAGE

    Test data from outside with IS NUMERIC before any statement treats it as a number.

  2. Question 2 of 12 Which zoned PIC 9(5) value passed IS NUMERIC in the run?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: 01234

    Only the value made of five digits passed; a letter or a space anywhere fails the test.

  3. Question 3 of 12 A signed packed field from z/OS holds X'12345F'. It fails IS NUMERIC in your GnuCOBOL tests but passes on z/OS. What explains it?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: GnuCOBOL's default dialect rejects the sign F in a signed item, while the IBM rules and -std=ibm accept it

    Build the tests with the dialect of production, or the class test answers a different question.

  4. Question 4 of 12 How does GnuCOBOL 3.2 treat an invalid character in a DISPLAY number when no check is enabled?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: It uses only the character's second half-byte as the digit

    The NEWS also warns that results differ from earlier versions, so unchecked bad data gives different answers on different releases.

  5. Question 5 of 12 In the build without checks, what did ADD of the packed bytes X'1234AC' give to a total of zero?

    Type a number.

    A hint

    The half-byte A was read as 10.

    Show the answer: Question 5 of 12

    Answer: 12350

    The digits 1, 2, 3, 4 and 10 make 12340 + 10, which is 12350, and the run printed it without a word.

  6. Question 6 of 12 A batch job built without checks produced plausible totals from a feed full of bad amounts. Which build would have exposed it?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: One with -fec=EC-DATA-INCOMPATIBLE or -debug, which stops at the first bad field

    Turn the data check on in test and in any run where a silent wrong total costs more than a stop.

  7. Question 7 of 12 What is the default of IBM's NUMPROC option, and what does it accept?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: NUMPROC(NOPFD), which accepts any valid sign

    NUMPROC(NOPFD) is the safe default for data from outside; PFD saves time only when every sign is preferred.

  8. Question 8 of 12 What does IBM's NUMCHECK(ZON,PAC) option add to a program?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Implicit numeric class tests on zoned and packed sending items, which report bad data at run time

    NUMCHECK is z/OS's nearest relative of GnuCOBOL's -fec=EC-DATA-INCOMPATIBLE, at a cost IBM calls much slower.

  9. Question 9 of 12 A job that abends with S0C7 on z/OS is moved to GnuCOBOL with default options and now finishes. Is the move done?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: No; the bad data is now added silently, so turn on the data check and validate the input

    An abend that disappears in a port is a finding, not a fix: find the bad records and reject them on purpose.

  10. Question 10 of 12 The condition name TX-CODE-OK lists the codes D and W. What kind of validation is that?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: An allowlist, which accepts only what is listed

    An allowlist rejects the code X and any code nobody thought of, which a list of bad codes would let through.

  11. Question 11 of 12 Why did validate.cob write bad records to a file instead of stopping at the first one?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: So one bad record does not stop the good ones, and every bad one is listed with its reason for correction

    Routing keeps the job's good work and turns bad data into a list someone can act on.

  12. Question 12 of 12 A new rule refuses withdrawals above 20,000.00 with reason 05. Where does the check go in CHECK-AND-ROUTE?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: After IS NUMERIC and the code check, so it compares only a valid amount of a known type

    Order the checks from the bytes to the meaning: present, known, numeric, then the business rules.

Add a fifth reason code

Add a check to validate.cob that refuses withdrawals above 20,000.00 with reason 05, after the range check. Add a record A10009W2500000 to transactions.txt, predict both output files, then run cobc -x validate.cob && ./validate and compare.

Interview questions

Warm-up (fresher to 2–5 years): what is an S0C7 abend, and what usually causes it?

S0C7 is the z/OS system completion code for a data exception: a decimal instruction met an operand that is not valid decimal data, so the program ends abnormally. In COBOL it usually means a zoned or packed field holds bytes that are not digits or not a valid sign: spaces or LOW-VALUES in an amount, a record read with the wrong layout, a group MOVE that put text into a number, or a field that was never initialised. IBM’s INVDATA page describes such data: invalid digits, an invalid sign code or invalid zone bits. The fix is to find the record and the field, usually from the dump, then validate input with IS NUMERIC where it enters and route bad records to a reject file. NUMCHECK can add such tests automatically while testing. A GnuCOBOL build without checks does not abend; it computes with the bad bytes.

Two harder questions apply these ideas: a nightly job that abends after a new feed, and a port to GnuCOBOL whose abends disappeared. Work out your answer first, then open each first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A nightly batch that has run for years abends with S0C7 on the first night after a new partner starts sending transactions. The job processes 400,000 records. How do you find the bad record and field quickly, keep the night's run going, and stop it from happening again?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A COBOL job is moved from z/OS to GnuCOBOL. On z/OS it occasionally abended with S0C7; on GnuCOBOL it has never failed, and the team calls that an improvement. Is it? What would you check, and how would you make the new platform behave safely?

    Model answer, Pro. First step and follow-ups

Organisations named on this page

MySmartCoPilot is independent of IBM, the GnuCOBOL project and the OWASP Foundation. The class test, the options and the system codes are cited from their documentation, listed under References. The outputs on this page are GnuCOBOL 3.2 runs on synthetic data.

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does the class condition IS NUMERIC check?

    The bytes, against the item's PICTURE and USAGE: a digit in every zoned position or packed digit half-byte, and a sign the dialect accepts. It says nothing about the value's range.

  2. Do spaces in an amount field pass IS NUMERIC?

    No: X'2020202020' in a zoned field fails, and so do EBCDIC spaces, X'404040', in a packed field, whose sign half-byte is 0.

  3. Does X'12345F' pass IS NUMERIC in a signed packed field?

    It depends on the dialect. GnuCOBOL's default rejects the unsigned sign F there; -std=ibm accepts it, as Enterprise COBOL does under NUMPROC(NOPFD) with NUMCLS(PRIM).

  4. What does a GnuCOBOL 3.2 build without checks do when ADD meets an amount of spaces?

    It reads only the second half-byte of each character, so a space counts as 0 and the X of 1234X as 8. The total is wrong and no message appears.

  5. Which GnuCOBOL builds stop at invalid numeric data?

    A -debug build, with every run-time check, or one with just -fec=EC-DATA-INCOMPATIBLE. The stop names the field, its bytes, the statement and its line.

  6. What is an S0C7 abend?

    The z/OS data exception: a decimal instruction met bytes that are not valid digits or signs, such as spaces in a packed amount, and the step ends abnormally.

  7. What does IBM's NUMCHECK option do?

    It adds numeric class tests to zoned and packed sending items: with MSG it warns, with ABD it ends the run. IBM warns that it is much slower than NONUMCHECK, which makes it a tool for test runs and a new feed's first runs.

  8. How should a batch program treat a record with a bad amount?

    Check every record where it enters, from its bytes to its business rules, and send each failure, with its reason code, to a reject file. The good records still run, and a return code reports the rejects.

Key takeaways

  • IS NUMERIC tests the bytes against the PICTURE and USAGE: spaces, 1234X, X’1234AC’ and X’404040’ failed it, and X’12345F’ passed only under the IBM rules.
  • Without checks GnuCOBOL 3.2 computed with bad bytes, spaces as 0 and 1234X as 12348, while -debug or -fec=EC-DATA-INCOMPATIBLE stopped at the statement and named the field.
  • On z/OS invalid digits or signs in decimal data cause the 0C7 data exception; NUMPROC decides the signs accepted and NUMCHECK adds class tests at a cost.
  • Validate each record where it enters, in order from the bytes to the business rules, and route failures to a reject file with a reason and a return code, so the good records still run.

Terms in this lesson

data exception
The z/OS program check of system completion code 0C7, raised when decimal arithmetic meets a zoned or packed operand with invalid digits or an invalid sign. It ends the step unless the program prevents it.
reject file
A file to which a program writes the input records it refuses, each with a reason code and its bytes as they arrived, so the run can go on and the records can be corrected and sent again.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.