PERFORM and Structured Programming

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 inline PERFORM and the TEST BEFORE/TEST AFTER choice come from COBOL 85, EXIT PERFORM, EXIT PARAGRAPH and EXIT SECTION from COBOL 2002 and PERFORM UNTIL EXIT is the newest addition; against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026), whose Language Reference documents all of them; 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 nothing different unless the text says so). The programs that are deliberately wrong say so, and the compiler messages shown are the ones cobc (GnuCOBOL) 3.2.0 printed.

COBOL has no for, while or function keyword. PERFORM plays all three roles: it runs a paragraph or section as a subroutine (control returns to the statement after the PERFORM), it repeats a procedure or a block of statements, and it is the only loop the language needs. This page covers its forms, the statements that leave a paragraph, a section or a loop early, and how a batch program is organized around them. Conditions are on Conditions, IF and EVALUATE; paragraphs and sections are introduced on Program Structure and Divisions; subscripts and indexes used as loop counters are on Tables and OCCURS. The examples use the Circulation public-library scenario, in particular the transaction file LOANTRAN with the copybook LOANREC.

Where the book is outdated: Taulli’s Modern Mainframe Development (2022) lists the PERFORM variants and GO TO in its language tour, in the style of the COBOL 85 programs that still dominate mainframe shops. It does not present EXIT PERFORM [CYCLE], EXIT PARAGRAPH and EXIT SECTION as the structured way out of a loop or paragraph, and it cannot cover PERFORM UNTIL EXIT, which Enterprise COBOL added in version 6.3 (through a PTF) and the 2023 standard adopted. Modern code needs neither GO TO nor ALTER for any control flow; the last section shows each old construct next to its replacement.

The PERFORM statement at a glance

Form What it does Origin

PERFORM para

Runs the paragraph once and returns

Every COBOL

PERFORM para-1 THRU para-2

Runs the paragraphs from para-1 to the end of para-2, once

Every COBOL

PERFORM section

Runs every paragraph of the section, once

Every COBOL

PERFORM n TIMES

Repeats a procedure or an inline block n times (n is a literal or an integer item)

Every COBOL

PERFORM UNTIL condition

Repeats until the condition is true; the condition is tested first (TEST BEFORE, the default) or last (TEST AFTER)

Every COBOL

PERFORM VARYING i FROM a BY b UNTIL condition

Counted loop that sets, steps and tests a counter; AFTER phrases nest further counters

Every COBOL

PERFORM ... END-PERFORM with no procedure name

Inline form: the statements between PERFORM and END-PERFORM are the body

COBOL 85

EXIT PERFORM [CYCLE]

Leaves, or continues with the next iteration of, the innermost inline PERFORM

COBOL 2002

PERFORM UNTIL EXIT

Loop with no end condition; only an explicit escape ends it

COBOL 2023; Enterprise COBOL 6.3 (PTF); GnuCOBOL 3.2

Out-of-line and inline PERFORM

PERFORM comes in two shapes that cannot be mixed. An out-of-line PERFORM names a procedure (PERFORM OPEN-DESK): control goes to the first statement of that paragraph or section and returns to the statement after the PERFORM when the last statement of the paragraph (or of the last paragraph of the section) has run. An inline PERFORM names no procedure and carries its body between PERFORM and END-PERFORM; the body runs in place. The TIMES, UNTIL and VARYING phrases work in both shapes; the only exception is the AFTER phrase of VARYING, which the Language Reference allows only when a procedure name is given.

PERFORM procedure-name-1 [THRU procedure-name-2]  [phrase]
PERFORM [phrase]  imperative-statements  END-PERFORM

phrase:  integer-or-identifier TIMES
      |  [WITH TEST BEFORE|AFTER]  UNTIL condition
      |  UNTIL EXIT
      |  [WITH TEST BEFORE|AFTER]  VARYING counter FROM start BY step
                                     UNTIL condition
                                     [AFTER counter FROM start BY step UNTIL condition] ...

The rules that matter in daily work:

  • A paragraph name is the end of the performed range. PERFORM A runs paragraph A and nothing of the paragraph that follows it. For a section, the range is every paragraph in it.

  • THRU (or THROUGH) extends the range to the end of a second paragraph that must appear after the first. Ranges built with THRU are discouraged (see the section on structured style); a section is the better way to group paragraphs.

  • A paragraph that is reached by falling down from the paragraph above runs without any PERFORM being involved: control "passes through the exit point to the next executable statement" as if no PERFORM referred to the paragraph. This is the commonest beginner bug (see the pitfalls).

  • Performed ranges can nest, but the inner range must lie completely inside or completely outside the outer one. A PERFORM must not cause itself to be executed: the Language Reference calls a recursive PERFORM unpredictable, and GnuCOBOL 3.2 compiled such a program without a word. Recursion belongs to programs declared RECURSIVE and to CALL, not to PERFORM.

  • TIMES takes a literal or an integer item, at most 999,999,999 in Enterprise COBOL. If the item is zero or negative the body is skipped, and changing the item inside the body does not change the number of iterations.

  • The INLINE compiler directive in Enterprise COBOL lets the compiler decide whether a performed procedure may be inlined; it never changes what the program does.

The first program runs every basic form, one after the other, on the opening and closing duties of the library desk.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PERFFORMS.
      * Every basic form of PERFORM, with the Circulation desk as theme.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-COUNT        PIC 9(2) VALUE 0.
       01  WS-SHELF        PIC 9(1).
       01  WS-SLOT         PIC 9(1).
       01  WS-N            PIC 9(1) VALUE 3.
       PROCEDURE DIVISION.
       MAIN-LINE.
           DISPLAY "1. paragraph:"
           PERFORM OPEN-DESK
           DISPLAY "2. THRU range:"
           PERFORM OPEN-DESK THRU OPEN-DESK-EXIT
           DISPLAY "3. section:"
           PERFORM CLOSING-DUTIES
           DISPLAY "4. TIMES:"
           PERFORM STAMP-DUE-SLIP 2 TIMES
           DISPLAY "5. inline TIMES, count changed inside:"
           PERFORM WS-N TIMES
               ADD 1 TO WS-COUNT
               MOVE 1 TO WS-N
           END-PERFORM
           DISPLAY "   iterations=" WS-COUNT
           DISPLAY "6. UNTIL (test before):"
           MOVE 5 TO WS-COUNT
           PERFORM UNTIL WS-COUNT >= 3
               ADD 1 TO WS-COUNT
           END-PERFORM
           DISPLAY "   count=" WS-COUNT
           DISPLAY "7. WITH TEST AFTER:"
           PERFORM WITH TEST AFTER UNTIL WS-COUNT >= 3
               ADD 1 TO WS-COUNT
           END-PERFORM
           DISPLAY "   count=" WS-COUNT
           DISPLAY "8. VARYING:"
           PERFORM VARYING WS-SHELF FROM 1 BY 1 UNTIL WS-SHELF > 3
               DISPLAY "   shelf " WS-SHELF
           END-PERFORM
           DISPLAY "   after loop shelf=" WS-SHELF
           DISPLAY "9. VARYING ... AFTER (slot is the inner loop):"
           PERFORM SHOW-SLOT
                   VARYING WS-SHELF FROM 1 BY 1 UNTIL WS-SHELF > 2
                   AFTER WS-SLOT FROM 1 BY 1 UNTIL WS-SLOT > 3
           STOP RUN.

       OPEN-DESK.
           DISPLAY "   switch on the terminal".
       OPEN-DESK-EXIT.
           EXIT.

       CLOSING-DUTIES SECTION.
       COUNT-CASH.
           DISPLAY "   count the cash drawer".
       LOCK-DOORS.
           DISPLAY "   lock the doors".

       SHOW-SLOT.
           DISPLAY "   shelf " WS-SHELF " slot " WS-SLOT.

       SLIPS SECTION.
       STAMP-DUE-SLIP.
           DISPLAY "   stamp a due-date slip".

Output (compiled with cobc -x -Wall, no warnings; -std=ibm printed the same):

1. paragraph:
   switch on the terminal
2. THRU range:
   switch on the terminal
3. section:
   count the cash drawer
   lock the doors
   shelf 0 slot 0
4. TIMES:
   stamp a due-date slip
   stamp a due-date slip
5. inline TIMES, count changed inside:
   iterations=03
6. UNTIL (test before):
   count=05
7. WITH TEST AFTER:
   count=06
8. VARYING:
   shelf 1
   shelf 2
   shelf 3
   after loop shelf=4
9. VARYING ... AFTER (slot is the inner loop):
   shelf 1 slot 1
   shelf 1 slot 2
   shelf 1 slot 3
   shelf 2 slot 1
   shelf 2 slot 2
   shelf 2 slot 3

Three details of that output are worth remembering. In case 5 the counter WS-N was changed to 1 inside the body, yet the body still ran three times: TIMES evaluates its count once, when the statement starts. In cases 6 and 7 the condition was already true when the loop was reached (the counter was 5, and later 6, against a limit of 3): with TEST BEFORE the body never ran, with TEST AFTER it ran once, so WITH TEST AFTER is COBOL’s do-while loop. And in case 8 the counter ends at 4, not 3: VARYING adds the step and then tests, so on exit the counter holds the first value that satisfied the condition.

VARYING and the AFTER phrases

PERFORM VARYING sets the counter to the FROM value, tests the UNTIL condition, runs the body, adds the BY value and tests again. Each AFTER phrase adds a counter that runs through its complete cycle for every step of the counter before it, so case 9 above is a pair of nested loops: WS-SLOT is the inner one. The Language Reference gives the exact sequence for two and three counters, and notes that the format can serially search an entire seven-dimensional table. At the end, each AFTER counter holds its FROM value again, and the first counter holds the value that ended the loop (or its FROM value if the condition was true from the start). The counters can be subscripts, index-names or numeric items, and a subscripted counter or condition operand is re-evaluated at every test.

The counter must be able to reach the value that satisfies the condition. A PIC 9(1) counter in UNTIL WS-I > 9 can never exceed 9: adding 1 to 9 truncates to 0 and the loop starts again. GnuCOBOL 3.2 recognizes this and warns at compile time (the loop below carries a second condition only so that the demonstration stops):

       IDENTIFICATION DIVISION.
       PROGRAM-ID. VARYEND.
      * Where the varied item stands after the loop, and a loop whose
      * counter is too small to ever pass the limit (guarded here).
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-I            PIC 9(1).
       01  WS-GUARD        PIC 9(2) VALUE 0.
       PROCEDURE DIVISION.
       MAIN-LINE.
           PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 9
                   OR WS-GUARD >= 12
               ADD 1 TO WS-GUARD
           END-PERFORM
           DISPLAY "guard=" WS-GUARD " i=" WS-I
           STOP RUN.

Compiled with cobc -x -Wall (the message is identical with -std=ibm), then run:

vary.cob: in paragraph 'MAIN-LINE':
vary.cob:11: warning: 'WS-I' may not be GREATER THAN 9 [-Wconstant-expression]
    9 |        PROCEDURE DIVISION.
   10 |        MAIN-LINE.
   11 >            PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 9
   12 |                    OR WS-GUARD >= 12
   13 |                ADD 1 TO WS-GUARD
guard=12 i=3

The counter went 1 to 9, wrapped to 0, and the guard stopped the loop on the twelfth iteration, leaving WS-I at 3. Without the guard the program would never end. Size the counter one digit larger than the largest value in the condition.

Leaving early: the EXIT statements

COBOL 2002 added statements that leave a paragraph, a section or an inline loop without GO TO:

Statement Effect

EXIT PARAGRAPH

Leaves the current paragraph at once; the statements after it in the paragraph do not run. Control moves to the end of the paragraph, so a PERFORM of it returns normally.

EXIT SECTION

Leaves the current section at once, skipping the remaining statements and paragraphs of the section.

EXIT PERFORM

Only inside an inline PERFORM: continues after the matching END-PERFORM, ending the loop. In a nested loop it ends the innermost one.

EXIT PERFORM CYCLE

Only inside an inline PERFORM: jumps to the end of the body so that the loop goes on with its next iteration (the VARYING step and the UNTIL test still happen).

PERFORM ... UNTIL EXIT

A loop whose condition never becomes true. It must contain a statement that really escapes it.

paragraph-name. EXIT.

The old "format 1" EXIT: a no-operation that only gives a paragraph name to the end of a range. Enterprise COBOL treats it as CONTINUE.

EXIT PERFORM outside an inline PERFORM is a compile-time error. EXIT PROGRAM and EXIT METHOD are different statements that end a called program or a method; they are covered with CALL on Subprograms, CALL and Linkage. Enterprise COBOL does not support EXIT FUNCTION.

The program below reports the fines of four members stored in a small table, using each form once.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PERFEXITS.
      * EXIT PARAGRAPH, EXIT SECTION, EXIT PERFORM [CYCLE] and
      * PERFORM UNTIL EXIT, using member fines from the Circulation desk.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-FINE-TABLE.
           05  WS-FINE-VALUES.
               10  FILLER      PIC 9(3)V99 VALUE 0.
               10  FILLER      PIC 9(3)V99 VALUE 12.50.
               10  FILLER      PIC 9(3)V99 VALUE 0.
               10  FILLER      PIC 9(3)V99 VALUE 50.00.
           05  WS-FINE REDEFINES WS-FINE-VALUES
                               PIC 9(3)V99 OCCURS 4 TIMES.
       01  WS-IX               PIC 9(1).
       01  WS-TRIES            PIC 9(1) VALUE 0.
       01  WS-SHOW             PIC ZZ9.99.
       PROCEDURE DIVISION.
       MAIN-LINE.
           DISPLAY "A. EXIT PARAGRAPH:"
           MOVE 2 TO WS-IX
           PERFORM REPORT-ONE-FINE
           MOVE 1 TO WS-IX
           PERFORM REPORT-ONE-FINE
           DISPLAY "B. EXIT SECTION:"
           PERFORM CHECK-DESK
           DISPLAY "C. EXIT PERFORM CYCLE (skip zero) / EXIT PERFORM:"
           PERFORM VARYING WS-IX FROM 1 BY 1 UNTIL WS-IX > 4
               IF WS-FINE (WS-IX) = 0
                   EXIT PERFORM CYCLE
               END-IF
               IF WS-FINE (WS-IX) > 40
                   DISPLAY "   member " WS-IX " over limit, stop"
                   EXIT PERFORM
               END-IF
               MOVE WS-FINE (WS-IX) TO WS-SHOW
               DISPLAY "   member " WS-IX " owes " WS-SHOW
           END-PERFORM
           DISPLAY "   loop ended with index " WS-IX
           DISPLAY "D. PERFORM UNTIL EXIT:"
           PERFORM UNTIL EXIT
               ADD 1 TO WS-TRIES
               DISPLAY "   attempt " WS-TRIES
               IF WS-TRIES = 3
                   EXIT PERFORM
               END-IF
           END-PERFORM
           DISPLAY "   done"
           STOP RUN.

       REPORT-ONE-FINE.
           IF WS-FINE (WS-IX) = 0
               DISPLAY "   member " WS-IX " owes nothing"
               EXIT PARAGRAPH
           END-IF
           MOVE WS-FINE (WS-IX) TO WS-SHOW
           DISPLAY "   member " WS-IX " owes " WS-SHOW.

       CHECK-DESK SECTION.
       CHECK-LIGHTS.
           DISPLAY "   lights checked"
           IF WS-TRIES = 0
               EXIT SECTION
           END-IF
           DISPLAY "   (not reached on the first call)".
       CHECK-DOORS.
           DISPLAY "   doors checked (not reached either)".

Output (cobc -x -Wall printed no warnings; -std=ibm printed the same):

A. EXIT PARAGRAPH:
   member 2 owes  12.50
   member 1 owes nothing
B. EXIT SECTION:
   lights checked
C. EXIT PERFORM CYCLE (skip zero) / EXIT PERFORM:
   member 2 owes  12.50
   member 4 over limit, stop
   loop ended with index 4
D. PERFORM UNTIL EXIT:
   attempt 1
   attempt 2
   attempt 3
   done

In part C, EXIT PERFORM CYCLE skipped the members who owe nothing and EXIT PERFORM ended the loop at the first fine over the limit; the index stayed at 4 because the VARYING step does not run after EXIT PERFORM. In part B EXIT SECTION skipped the rest of CHECK-DESK, including the whole second paragraph.

PERFORM UNTIL EXIT

PERFORM UNTIL EXIT behaves exactly like PERFORM UNTIL condition with a condition that is never true. The loop therefore needs an escape inside it. The rules from the Language Reference:

  • UNTIL EXIT must not be combined with TEST BEFORE, TEST AFTER or VARYING. GnuCOBOL 3.2 rejects it with VARYING (error: syntax error, unexpected EXIT) but accepts it silently with WITH TEST AFTER, so do not rely on the compiler to find that mistake.

  • For an inline loop, the escape is EXIT PERFORM. EXIT PERFORM CYCLE is not an escape.

  • For an out-of-line loop, the escape is GOBACK or STOP RUN. EXIT PARAGRAPH inside the performed paragraph and EXIT SECTION inside the performed section do not escape the loop: they only end that pass through the paragraph or section, and the loop starts again.

Where it helps: a read loop that has to test the result of the READ before using the record, without a priming read (see the next section), and loops with several unrelated exit conditions, each of which becomes an IF ... EXIT PERFORM.

The batch skeleton

Almost every batch program in the Circulation back office has the same shape: initialize, process the transactions until end of file, terminate. The paragraphs are named with a number prefix that fixes their order on the listing, one paragraph does one job, and the main line only performs paragraphs.

flowchart TD START(["0000-MAIN-LINE"]) --> INIT["1000-INITIALIZE: open files"] INIT --> PRIME["8000-READ-LOAN: priming read"] PRIME --> TEST{"END-OF-LOANS?"} TEST -- "no" --> PROC["2000-PROCESS-LOAN: apply the record"] PROC --> NEXT["8000-READ-LOAN: read the next record"] NEXT --> TEST TEST -- "yes" --> TERM["3000-TERMINATE: close files, print totals"] TERM --> STOP(["STOP RUN"])

The first read happens before the loop (the priming read) so that the loop test always has a record, or the end-of-file switch, to look at. The last statement of the processing paragraph reads the next record, so the loop test at the top sees it. The same READ paragraph serves both places. The program counts the day’s transactions on LOANTRAN.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANBATCH.
      * Batch skeleton: initialize, priming read, process until end of
      * file, terminate. Counts the day's transactions on LOANTRAN.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT LOAN-FILE ASSIGN TO "LOANTRAN"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  LOAN-FILE.
           COPY LOANREC.
       WORKING-STORAGE SECTION.
       01  WS-SWITCHES.
           05  WS-EOF-SW           PIC X VALUE "N".
               88  END-OF-LOANS    VALUE "Y".
       01  WS-COUNTS.
           05  WS-READ-CNT         PIC 9(4) VALUE 0.
           05  WS-CHECKOUT-CNT     PIC 9(4) VALUE 0.
           05  WS-RETURN-CNT       PIC 9(4) VALUE 0.
           05  WS-BAD-CNT          PIC 9(4) VALUE 0.
       PROCEDURE DIVISION.
       0000-MAIN-LINE.
           PERFORM 1000-INITIALIZE
           PERFORM 2000-PROCESS-LOAN UNTIL END-OF-LOANS
           PERFORM 3000-TERMINATE
           STOP RUN.

       1000-INITIALIZE.
           OPEN INPUT LOAN-FILE
           PERFORM 8000-READ-LOAN.

       2000-PROCESS-LOAN.
           EVALUATE TRUE
               WHEN LOAN-CHECKOUT
                   ADD 1 TO WS-CHECKOUT-CNT
               WHEN LOAN-RETURN
                   ADD 1 TO WS-RETURN-CNT
               WHEN OTHER
                   ADD 1 TO WS-BAD-CNT
           END-EVALUATE
           PERFORM 8000-READ-LOAN.

       3000-TERMINATE.
           CLOSE LOAN-FILE
           DISPLAY "records read   " WS-READ-CNT
           DISPLAY "checkouts      " WS-CHECKOUT-CNT
           DISPLAY "returns        " WS-RETURN-CNT
           DISPLAY "rejected       " WS-BAD-CNT.

       8000-READ-LOAN.
           READ LOAN-FILE
               AT END
                   SET END-OF-LOANS TO TRUE
               NOT AT END
                   ADD 1 TO WS-READ-CNT
           END-READ.

Output (cobc -x -Wall printed no warnings; -std=ibm printed the same). LOANTRAN holds three checkouts and one return:

records read   0004
checkouts      0003
returns        0001
rejected       0000

The same job without a priming read puts the READ inside a PERFORM UNTIL EXIT loop and leaves it with EXIT PERFORM when the end-of-file switch is set. Only the main line and the two paragraphs that lost their READ differ from the first version:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANBATCH2.
      * The same job without a priming read: one READ inside a
      * PERFORM UNTIL EXIT loop (COBOL 2023; Enterprise COBOL 6.3+).
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT LOAN-FILE ASSIGN TO "LOANTRAN"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  LOAN-FILE.
           COPY LOANREC.
       WORKING-STORAGE SECTION.
       01  WS-SWITCHES.
           05  WS-EOF-SW           PIC X VALUE "N".
               88  END-OF-LOANS    VALUE "Y".
       01  WS-COUNTS.
           05  WS-READ-CNT         PIC 9(4) VALUE 0.
           05  WS-CHECKOUT-CNT     PIC 9(4) VALUE 0.
           05  WS-RETURN-CNT       PIC 9(4) VALUE 0.
           05  WS-BAD-CNT          PIC 9(4) VALUE 0.
       PROCEDURE DIVISION.
       0000-MAIN-LINE.
           PERFORM 1000-INITIALIZE
           PERFORM UNTIL EXIT
               PERFORM 8000-READ-LOAN
               IF END-OF-LOANS
                   EXIT PERFORM
               END-IF
               PERFORM 2000-PROCESS-LOAN
           END-PERFORM
           PERFORM 3000-TERMINATE
           STOP RUN.

       1000-INITIALIZE.
           OPEN INPUT LOAN-FILE.

       2000-PROCESS-LOAN.
           EVALUATE TRUE
               WHEN LOAN-CHECKOUT
                   ADD 1 TO WS-CHECKOUT-CNT
               WHEN LOAN-RETURN
                   ADD 1 TO WS-RETURN-CNT
               WHEN OTHER
                   ADD 1 TO WS-BAD-CNT
           END-EVALUATE.

       3000-TERMINATE.
           CLOSE LOAN-FILE
           DISPLAY "records read   " WS-READ-CNT
           DISPLAY "checkouts      " WS-CHECKOUT-CNT
           DISPLAY "returns        " WS-RETURN-CNT
           DISPLAY "rejected       " WS-BAD-CNT.

       8000-READ-LOAN.
           READ LOAN-FILE
               AT END
                   SET END-OF-LOANS TO TRUE
               NOT AT END
                   ADD 1 TO WS-READ-CNT
           END-READ.

It prints exactly the same four lines:

records read   0004
checkouts      0003
returns        0001
rejected       0000

Which style to use is mostly a matter of team convention. The priming-read version works on every compiler ever written and is what a maintenance programmer on a mainframe expects; the UNTIL EXIT version has one READ instead of two and keeps the end-of-file test next to the read, but needs Enterprise COBOL 6.3 with the PTF or later, or a 2023-standard compiler.

Structure rules for the skeleton

  • The main line contains PERFORM statements and one STOP RUN (or GOBACK in a subprogram) and nothing else of substance. Because the paragraphs follow the main line, the end of the main line must be an explicit STOP RUN: without it control falls into the first performed paragraph.

  • One period per paragraph, at its end. Inside, use the scope terminators (END-IF, END-EVALUATE, END-READ, END-PERFORM) so that a stray period cannot silently close a statement early.

  • Give a paragraph one entry and one exit: it is entered by a PERFORM and left at its last statement. Use EXIT PARAGRAPH to leave early rather than a GO TO to an exit paragraph.

  • Keep paragraph names unique and numbered or prefixed by area, so a cross-reference listing is readable; sections are optional and only worth having when a group of paragraphs is performed as a unit.

  • Put the end-of-file test in a level 88 condition name (END-OF-LOANS), set by the AT END branch of the READ.

Pitfall: falling through into a paragraph

The main line below has no STOP RUN. This program is wrong on purpose:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. FALLTHRU.
      * Deliberately wrong: MAIN-LINE has no STOP RUN or GOBACK.
       PROCEDURE DIVISION.
       MAIN-LINE.
           DISPLAY "main line"
           PERFORM PRINT-REPORT.
       PRINT-REPORT.
           DISPLAY "report printed".

It compiles without a message and prints:

main line
report printed
report printed

PRINT-REPORT ran once because of the PERFORM, and a second time because MAIN-LINE ended and control simply moved on to the next paragraph. GnuCOBOL 3.2 gives no diagnostic; in a real program the second run might add every record twice. The fix is STOP RUN (or GOBACK) as the last statement of the main line.

Pitfall: compiler diagnostics for malformed PERFORMs

The messages below are what cobc -x -Wall printed for four small invalid programs, shortened to the line that matters:

error: PERFORM statement not terminated by END-PERFORM
        PERFORM UNTIL WS-I > 2
            ADD 1 TO WS-I.

error: EXIT PERFORM is only valid with inline PERFORM
        EXIT PERFORM

error: syntax error, unexpected ADD, expecting UNTIL or VARYING
        PERFORM WITH TEST BEFORE
            ADD 1 TO WS-I

warning: EARLY-PARA is defined before LATE-PARA [-Wsuspicious-perform-thru]
        PERFORM LATE-PARA THRU EARLY-PARA

The first is the period inside an inline PERFORM: the period ends the whole statement, so the END-PERFORM is missing. The last one is a THRU range whose end comes before its start in the source.

Structured style

Structured programming in COBOL comes down to a few habits, most of them already visible in the skeleton:

  • Control flow uses only sequence, IF/EVALUATE and PERFORM; no GO TO, no ALTER.

  • Every performed paragraph has a single purpose and a name that says it (2100-APPLY-CHECKOUT, not PARA-17).

  • PERFORM ... THRU ranges, and the exit paragraphs that go with them, are replaced by single paragraphs or by sections. A range depends on the physical order of the paragraphs, so inserting a paragraph in the wrong place changes what runs.

  • Loops use the inline form when the body is a few statements and an out-of-line paragraph when it is long or reused.

  • Exits from loops use EXIT PERFORM, EXIT PERFORM CYCLE and EXIT PARAGRAPH instead of jumping out.

Old code you may meet: GO TO, GO TO DEPENDING ON and ALTER

Programs written before 1985 are built from GO TO. They still run in production, and you will be asked to read them. The unconditional GO TO paragraph transfers control and never returns; GO TO p1 p2 p3 DEPENDING ON n jumps to the n-th named paragraph, and falls through to the next statement when n is outside 1 to 3; ALTER paragraph TO PROCEED TO other rewrites, at run time, the target of a paragraph that consists of a single GO TO, so what a GO TO does depends on which statements ran earlier. The 85 standard classed ALTER as obsolete and the 2002 standard dropped it; Enterprise COBOL still accepts it, but not in a RECURSIVE program (nor an altered GO TO in a program compiled with the THREAD option). Do not write any of this in new code.

The program below is old code you may meet; it is deliberately a tangle. The compiler accepts it with a warning about ALTER (warning: ALTER is obsolete in GnuCOBOL [-Wobsolete], worded in IBM COBOL (lax) under -std=ibm):

       IDENTIFICATION DIVISION.
       PROGRAM-ID. OLDSTYLE.
      * OLD CODE YOU MAY MEET: GO TO, GO TO DEPENDING ON and ALTER.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-CHOICE       PIC 9 VALUE 2.
       01  WS-TRAN-TYPE    PIC X VALUE "R".
       PROCEDURE DIVISION.
       MAIN-LINE.
           GO TO CHECKOUT-PARA RETURN-PARA RENEW-PARA
               DEPENDING ON WS-CHOICE.
           DISPLAY "choice out of range".
           GO TO FIRST-ACTION.
       CHECKOUT-PARA.
           DISPLAY "checkout".
           GO TO FIRST-ACTION.
       RETURN-PARA.
           DISPLAY "return".
           GO TO FIRST-ACTION.
       RENEW-PARA.
           DISPLAY "renew".
           GO TO FIRST-ACTION.
       FIRST-ACTION.
           GO TO FIRST-TIME.
       FIRST-TIME-DONE.
           PERFORM LATER-ACTION.
           STOP RUN.
       FIRST-TIME.
           DISPLAY "first-time work".
           ALTER FIRST-ACTION TO PROCEED TO FIRST-TIME-DONE.
           GO TO FIRST-ACTION.
       LATER-ACTION.
           DISPLAY "later work".
old.cob: in paragraph 'FIRST-TIME':
old.cob:30: warning: ALTER is obsolete in GnuCOBOL [-Wobsolete]
   28 |        FIRST-TIME.
   29 |            DISPLAY "first-time work".
   30 >            ALTER FIRST-ACTION TO PROCEED TO FIRST-TIME-DONE.
   31 |            GO TO FIRST-ACTION.
   32 |        LATER-ACTION.
return
first-time work
later work

Its modern form is an EVALUATE for the choice and a condition name for the once-only work, with PERFORM for the subroutine; it needs no GO TO and no ALTER, compiles without warnings and prints the same three lines:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. MODERN.
      * The modern form of OLDSTYLE: EVALUATE and a condition-name.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-CHOICE       PIC 9 VALUE 2.
       01  WS-FIRST-SW     PIC X VALUE "Y".
           88  FIRST-TIME  VALUE "Y".
       PROCEDURE DIVISION.
       MAIN-LINE.
           EVALUATE WS-CHOICE
               WHEN 1
                   DISPLAY "checkout"
               WHEN 2
                   DISPLAY "return"
               WHEN 3
                   DISPLAY "renew"
               WHEN OTHER
                   DISPLAY "choice out of range"
           END-EVALUATE
           IF FIRST-TIME
               DISPLAY "first-time work"
               MOVE "N" TO WS-FIRST-SW
           END-IF
           PERFORM LATER-ACTION
           STOP RUN.

       LATER-ACTION.
           DISPLAY "later work".
return
first-time work
later work

When you must change such a program, the safe course is to leave its control flow alone, change only the paragraph you need, and replace the tangle with PERFORM and EVALUATE as a separate, tested step. The migration side of this (how to find ALTER and GO TO DEPENDING ON in a large code base and what to replace them with) is covered on Legacy Features and Migration.

Standard, Enterprise COBOL and GnuCOBOL

Feature ISO/IEC 1989:2023 Enterprise COBOL 6.5 GnuCOBOL 3.2

Inline PERFORM …​ END-PERFORM

Yes (COBOL 85)

Yes

Yes

EXIT PERFORM [CYCLE], EXIT PARAGRAPH, EXIT SECTION

Yes (COBOL 2002)

Yes

Yes; rejected by -std=cobol85 (syntax error, unexpected Identifier)

PERFORM UNTIL EXIT

Yes (2023)

Yes, since 6.3 (PTF)

Yes

UNTIL EXIT with TEST BEFORE/TEST AFTER

Not allowed

Not allowed

TEST AFTER accepted silently; VARYING rejected

AFTER phrase without a procedure name (inline)

Language Reference of 6.5: not allowed

Not allowed

Accepted

Recursive PERFORM

Undefined

Unpredictable; no diagnostic documented

No diagnostic