30 min read

> "In COBOL, PERFORM is not merely a loop — it is the fundamental mechanism of program control."

Chapter 7: Iteration Patterns

"In COBOL, PERFORM is not merely a loop — it is the fundamental mechanism of program control." — Robert T. Grauer, Structured COBOL Programming

If conditional logic (Chapter 6) is how a COBOL program makes decisions, then iteration is how it does work. A banking system does not process one transaction — it processes 2.3 million per day, one after another. An insurance system does not adjudicate one claim — it adjudicates 500,000 per month. The PERFORM statement, in all its forms, is the engine that drives this repetitive processing.

In your first COBOL course, you likely used PERFORM paragraph-name and perhaps PERFORM ... UNTIL. In this chapter, we go much further. You will master every form of PERFORM, understand the critical differences between TEST BEFORE and TEST AFTER, learn when to use inline versus out-of-line PERFORMs, explore the controversial PERFORM THRU, and develop defensive programming habits that prevent the most feared bug in batch processing: the infinite loop.

The theme of this chapter is Defensive Programming. In a batch job processing millions of records, an infinite loop does not just waste CPU cycles — it can hold up an entire mainframe's job schedule, delay end-of-day processing, and trigger middle-of-the-night pages to on-call operations staff. Learning to write loops that always terminate is not optional — it is a professional survival skill.


7.1 The PERFORM Family — An Overview

COBOL provides six fundamental forms of PERFORM:

Form Purpose Example
Basic PERFORM Execute a paragraph once PERFORM 2100-VALIDATE
PERFORM ... TIMES Execute a fixed number of times PERFORM 2100-VALIDATE 5 TIMES
PERFORM ... UNTIL Execute until a condition is true PERFORM 2100-READ-RECORD UNTIL END-OF-FILE
PERFORM ... VARYING Counting loop with automatic increment PERFORM ... VARYING WS-IDX FROM 1 BY 1 UNTIL WS-IDX > 100
Inline PERFORM Loop body is written in-line PERFORM UNTIL ... END-PERFORM
PERFORM ... THRU Execute a range of paragraphs PERFORM 2100-START THRU 2199-END

Each form exists for a reason, and choosing the right one makes your code clearer and safer.


7.2 Basic PERFORM — The Building Block

The simplest PERFORM executes a paragraph once and returns:

       PERFORM 2100-VALIDATE-ACCOUNT

This transfers control to paragraph 2100-VALIDATE-ACCOUNT, executes all statements in it, and returns to the statement after the PERFORM. It is the COBOL equivalent of a subroutine call (within a program).

Basic PERFORM as a Design Tool

The basic PERFORM is more than just a way to avoid duplicating code. It is a design tool — a way to express the structure of your program at a high level:

       1000-MAIN-PROCESS.
           PERFORM 1100-INITIALIZE
           PERFORM 1200-OPEN-FILES
           PERFORM 1300-PROCESS-ALL-RECORDS
           PERFORM 1400-WRITE-TOTALS
           PERFORM 1500-CLOSE-FILES
           STOP RUN.

This paragraph tells you everything about the program's structure in five lines. The details of how records are processed, how files are opened, how totals are written — all of that is delegated to subordinate paragraphs. This is top-down design in action, and we will explore it further in Chapter 8.

💡 Style Tip: Notice the verb-noun naming convention: INITIALIZE, OPEN-FILES, PROCESS-ALL-RECORDS. Each paragraph name tells you what it does. Maria Chen at GlobalBank requires all paragraph names to begin with a verb. "If it doesn't start with a verb," she says, "it isn't doing anything, and it shouldn't be a paragraph."


7.3 PERFORM ... TIMES — Fixed Repetition

When you know exactly how many iterations you need, PERFORM ... TIMES is the clearest way to express it:

       PERFORM 2100-PRINT-SEPARATOR 3 TIMES

This executes paragraph 2100-PRINT-SEPARATOR exactly three times. The count can also be a data item:

       PERFORM 2100-PROCESS-ITEM WS-ITEM-COUNT TIMES

If WS-ITEM-COUNT is zero or negative, the paragraph is not executed at all. This is safe — no need for a guard clause.

When to Use PERFORM ... TIMES

PERFORM ... TIMES is ideal for: - Printing repeated separator lines or headers - Initializing a fixed-size table with default values - Retrying an operation a fixed number of times

      * Retry a connection up to 3 times
       MOVE 0 TO WS-RETRY-COUNT
       MOVE 'N' TO WS-CONNECTED
       PERFORM 2100-ATTEMPT-CONNECT 3 TIMES
       IF WS-CONNECTED = 'N'
           PERFORM 9100-LOG-CONNECTION-FAILURE
       END-IF

Wait — there is a subtlety here. PERFORM ... TIMES always executes the specified number of times; it does not exit early if the connection succeeds. For early exit, we need PERFORM ... UNTIL:

       MOVE 0 TO WS-RETRY-COUNT
       MOVE 'N' TO WS-CONNECTED
       PERFORM 2100-ATTEMPT-CONNECT
           UNTIL WS-CONNECTED = 'Y'
           OR WS-RETRY-COUNT >= 3

This is a common mistake with PERFORM ... TIMES. Use it only when you truly need all iterations to execute.


7.4 PERFORM ... UNTIL — Condition-Controlled Loops

PERFORM ... UNTIL is the workhorse of COBOL batch processing. It executes a paragraph repeatedly until a condition becomes true:

       PERFORM 2100-READ-AND-PROCESS
           UNTIL END-OF-FILE

Here, END-OF-FILE is an 88-level condition name (as discussed in Chapter 6). The loop reads and processes records until the end of the file is reached.

TEST BEFORE vs. TEST AFTER

This is one of the most important — and most commonly misunderstood — distinctions in COBOL iteration. By default, PERFORM ... UNTIL uses TEST BEFORE semantics: the condition is tested before each iteration. You can also specify TEST AFTER: the condition is tested after each iteration.

      * TEST BEFORE (default) — condition tested BEFORE first execution
       PERFORM 2100-READ-RECORD
           WITH TEST BEFORE
           UNTIL END-OF-FILE

      * TEST AFTER — condition tested AFTER each execution
       PERFORM 2100-READ-RECORD
           WITH TEST AFTER
           UNTIL END-OF-FILE

The critical difference: with TEST BEFORE, if the condition is already true when the PERFORM is first encountered, the paragraph is never executed. With TEST AFTER, the paragraph is always executed at least once.

📊 Comparison:

TEST BEFORE (default) TEST AFTER
Condition checked Before each iteration After each iteration
Minimum executions 0 1
Analogous to while loop in C/Java do-while loop in C/Java
When to use When zero iterations is valid When at least one iteration is always needed

The Classic Read Loop Pattern

The most common COBOL loop pattern is the "priming read" pattern for sequential file processing:

       1300-PROCESS-ALL-RECORDS.
           PERFORM 2100-READ-NEXT-RECORD
           PERFORM 2200-PROCESS-RECORD
               UNTIL END-OF-FILE
           .

       2100-READ-NEXT-RECORD.
           READ TXN-FILE INTO WS-TXN-RECORD
               AT END
                   SET END-OF-FILE TO TRUE
               NOT AT END
                   ADD 1 TO WS-RECORD-COUNT
           END-READ
           .

       2200-PROCESS-RECORD.
           PERFORM 2210-VALIDATE-TXN
           PERFORM 2220-APPLY-TXN
           PERFORM 2100-READ-NEXT-RECORD
           .

⚠️ Critical Pattern: Notice the structure — the first READ (the "priming read") is performed before the loop. The loop body processes the current record and then reads the next one. This is necessary because PERFORM ... UNTIL with TEST BEFORE checks the condition before executing, so the first record must already be read before the loop begins.

This is one of the most important patterns in COBOL programming. Getting it wrong results in either skipping the first record or processing one record past the end of the file — both common bugs.

Alternative: TEST AFTER Read Loop

Some programmers prefer TEST AFTER to eliminate the priming read:

       1300-PROCESS-ALL-RECORDS.
           PERFORM 2200-PROCESS-RECORD
               WITH TEST AFTER
               UNTIL END-OF-FILE
           .

       2200-PROCESS-RECORD.
           READ TXN-FILE INTO WS-TXN-RECORD
               AT END
                   SET END-OF-FILE TO TRUE
               NOT AT END
                   PERFORM 2210-VALIDATE-TXN
                   PERFORM 2220-APPLY-TXN
           END-READ
           .

With TEST AFTER, the paragraph is always executed at least once, and the READ happens inside the loop. When the end of file is reached, the AT END clause sets the flag, and the loop exits.

⚖️ Debate: Which pattern is better? The priming read pattern is more traditional and explicit about the flow of control. The TEST AFTER pattern is more concise and avoids duplicating the READ call. Both are correct. At GlobalBank, Maria Chen's team uses the priming read pattern because it maps more cleanly to the batch processing lifecycle (initialize, read first, process loop, finalize). At MedClaim, James Okafor prefers TEST AFTER because it keeps the READ in one place. Whichever you choose, be consistent within your project.

🧪 Try It Yourself: Read Loop Patterns

Write both versions of a read loop — priming read with TEST BEFORE and the TEST AFTER alternative — for a program that reads a sequential file of employee records and counts how many employees are in each department. Use 88-level condition names for the end-of-file flag.


7.5 PERFORM ... VARYING — The Counting Loop

PERFORM ... VARYING is COBOL's counting loop, analogous to a for loop in other languages:

       PERFORM 2100-PROCESS-ELEMENT
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > 100

This executes paragraph 2100-PROCESS-ELEMENT with WS-IDX taking values 1, 2, 3, ..., 100.

Anatomy of PERFORM VARYING

PERFORM paragraph-name
    VARYING identifier-1 FROM value-1 BY value-2
    UNTIL condition
  • identifier-1: The loop counter (must be a numeric data item)
  • FROM value-1: Initial value (can be a literal or a data item)
  • BY value-2: Increment (can be a literal or data item; can be negative for counting down)
  • UNTIL condition: Loop termination condition

Counting Down

       PERFORM 2100-DISPLAY-COUNTDOWN
           VARYING WS-COUNTER FROM 10 BY -1
           UNTIL WS-COUNTER < 1

This counts down from 10 to 1.

Using Data Items for FROM and BY

The FROM and BY values can be data items, allowing dynamic loop control:

       PERFORM 2100-PROCESS-RANGE
           VARYING WS-IDX FROM WS-START-IDX BY WS-STEP
           UNTIL WS-IDX > WS-END-IDX

Table Traversal with PERFORM VARYING

One of the most common uses of PERFORM VARYING is traversing a table (array):

       01  WS-ACCT-TABLE.
           05  WS-ACCT-ENTRY OCCURS 500 TIMES.
               10  WS-ACCT-NUM     PIC X(10).
               10  WS-ACCT-BAL     PIC S9(11)V99.
               10  WS-ACCT-STATUS  PIC X(01).

       01  WS-IDX                  PIC 9(04).
       01  WS-TABLE-SIZE           PIC 9(04) VALUE 500.

      * Search for an account
       PERFORM 2100-SEARCH-ACCOUNT
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           OR WS-ACCT-NUM(WS-IDX) = WS-SEARCH-ACCT

💡 Important: When the loop exits because the condition WS-ACCT-NUM(WS-IDX) = WS-SEARCH-ACCT is true, WS-IDX contains the index of the found element. When it exits because WS-IDX > WS-TABLE-SIZE, the account was not found. Always check which condition caused the exit:

       PERFORM 2100-SEARCH-ACCOUNT
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           OR WS-ACCT-NUM(WS-IDX) = WS-SEARCH-ACCT

       IF WS-IDX > WS-TABLE-SIZE
           DISPLAY 'Account not found'
       ELSE
           DISPLAY 'Found at position ' WS-IDX
       END-IF

The VARYING Counter After the Loop

⚠️ Critical Gotcha: After a PERFORM VARYING completes, the loop counter may contain a value beyond the expected range. Consider:

       PERFORM 2100-PROCESS
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > 10

After the loop, WS-IDX contains 11, not 10. This is because the UNTIL condition is tested after incrementing. The loop sets WS-IDX to 11, tests 11 > 10 (true), and exits — but WS-IDX is already 11. This catches many programmers off guard. Always be aware of the counter's value after the loop.


7.6 PERFORM VARYING with AFTER — Nested Counting

The AFTER clause extends PERFORM VARYING to handle nested loops — essential for processing two-dimensional tables:

       PERFORM 2100-PROCESS-CELL
           VARYING WS-ROW FROM 1 BY 1
           UNTIL WS-ROW > WS-MAX-ROWS
           AFTER WS-COL FROM 1 BY 1
           UNTIL WS-COL > WS-MAX-COLS

This is equivalent to:

for ROW = 1 to MAX-ROWS
    for COL = 1 to MAX-COLS
        process cell(ROW, COL)

The AFTER identifier (WS-COL) varies faster — it completes its full range for each value of the VARYING identifier (WS-ROW). You can chain multiple AFTER clauses for three-dimensional or higher tables.

Example: Monthly Report Grid

At GlobalBank, the daily report program generates a 12-month x 5-account-type summary grid:

       01  WS-MONTHLY-TOTALS.
           05  WS-MONTH-ROW OCCURS 12 TIMES.
               10  WS-TYPE-COL OCCURS 5 TIMES.
                   15  WS-TXN-COUNT    PIC 9(07).
                   15  WS-TXN-TOTAL    PIC S9(13)V99.

       01  WS-MONTH-IDX            PIC 9(02).
       01  WS-TYPE-IDX             PIC 9(02).
       01  WS-GRAND-TOTAL          PIC S9(15)V99 VALUE ZERO.

      * Initialize all cells to zero
       PERFORM 2100-INIT-CELL
           VARYING WS-MONTH-IDX FROM 1 BY 1
           UNTIL WS-MONTH-IDX > 12
           AFTER WS-TYPE-IDX FROM 1 BY 1
           UNTIL WS-TYPE-IDX > 5

      * ... later, compute grand total
       PERFORM 2200-ADD-TO-GRAND-TOTAL
           VARYING WS-MONTH-IDX FROM 1 BY 1
           UNTIL WS-MONTH-IDX > 12
           AFTER WS-TYPE-IDX FROM 1 BY 1
           UNTIL WS-TYPE-IDX > 5
       .

       2100-INIT-CELL.
           MOVE 0 TO WS-TXN-COUNT(WS-MONTH-IDX, WS-TYPE-IDX)
           MOVE 0 TO WS-TXN-TOTAL(WS-MONTH-IDX, WS-TYPE-IDX)
           .

       2200-ADD-TO-GRAND-TOTAL.
           ADD WS-TXN-TOTAL(WS-MONTH-IDX, WS-TYPE-IDX)
               TO WS-GRAND-TOTAL
           .

7.7 Inline PERFORM vs. Out-of-Line PERFORM

COBOL-85 introduced inline PERFORM, where the loop body is written directly between PERFORM and END-PERFORM rather than in a separate paragraph:

      * Out-of-line (paragraph-based)
       PERFORM 2100-ACCUMULATE
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE

      * Inline
       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           ADD WS-AMOUNT(WS-IDX) TO WS-TOTAL
       END-PERFORM

When to Use Each

Use Inline PERFORM When... Use Out-of-Line PERFORM When...
Loop body is 1-5 statements Loop body is more than 5 statements
Logic is simple and local Logic is complex or reused elsewhere
No need to call from other places Multiple paragraphs need to invoke the same logic
Loop body doesn't contain nested loops Loop body contains complex nested logic
      * Good inline: simple accumulation
       MOVE 0 TO WS-TOTAL
       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-ENTRY-COUNT
           ADD WS-AMOUNT(WS-IDX) TO WS-TOTAL
       END-PERFORM

      * Bad inline: too much logic inline
       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-ENTRY-COUNT
           IF WS-STATUS(WS-IDX) = 'A'
               IF WS-AMOUNT(WS-IDX) > 0
                   ADD WS-AMOUNT(WS-IDX) TO WS-TOTAL
                   ADD 1 TO WS-ACTIVE-COUNT
                   IF WS-AMOUNT(WS-IDX) > WS-MAX-AMOUNT
                       MOVE WS-AMOUNT(WS-IDX)
                           TO WS-MAX-AMOUNT
                   END-IF
               END-IF
           END-IF
       END-PERFORM

The second example should be refactored to an out-of-line PERFORM:

       PERFORM 2100-PROCESS-ENTRY
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-ENTRY-COUNT

      * ... later in PROCEDURE DIVISION:
       2100-PROCESS-ENTRY.
           IF WS-STATUS(WS-IDX) = 'A'
           AND WS-AMOUNT(WS-IDX) > 0
               ADD WS-AMOUNT(WS-IDX) TO WS-TOTAL
               ADD 1 TO WS-ACTIVE-COUNT
               IF WS-AMOUNT(WS-IDX) > WS-MAX-AMOUNT
                   MOVE WS-AMOUNT(WS-IDX) TO WS-MAX-AMOUNT
               END-IF
           END-IF
           .

📊 Readability Guideline: Derek Washington uses the "scroll test" — if the inline PERFORM body does not fit on a single screen (approximately 25 lines), it should be extracted to a paragraph. This is not a hard rule, but it is a good heuristic.

🧪 Try It Yourself: Inline vs. Out-of-Line

Take the following out-of-line PERFORM and rewrite it as an inline PERFORM. Then decide: which version is more readable? Why?

       PERFORM 2100-PRINT-STARS
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-STAR-COUNT
       .
       2100-PRINT-STARS.
           DISPLAY '*'
           .

7.8 PERFORM THRU — Range Execution

PERFORM THRU (or PERFORM THROUGH — both spellings are accepted) executes a range of consecutive paragraphs:

       PERFORM 2100-VALIDATE-START
           THRU 2199-VALIDATE-EXIT

This executes all paragraphs from 2100-VALIDATE-START through 2199-VALIDATE-EXIT, in order.

The PERFORM THRU Debate

PERFORM THRU is one of the most debated constructs in COBOL programming. It has strong advocates and equally strong detractors.

Arguments for PERFORM THRU: - Provides a clean exit point pattern (a common idiom pairs each logical paragraph with an exit paragraph) - Allows GO TO to an exit paragraph within the THRU range, enabling guard clauses - Groups related paragraphs into a logical unit

Arguments against PERFORM THRU: - Makes it easy to accidentally add a paragraph within the THRU range that should not be executed - Ties paragraph ordering to execution semantics — reordering paragraphs can break the program - Can mask spaghetti code by hiding GO TO within the THRU range

      * Common PERFORM THRU pattern with exit paragraph
       PERFORM 2100-VALIDATE-ACCOUNT
           THRU 2199-VALIDATE-ACCOUNT-EXIT

      * In the PROCEDURE DIVISION:
       2100-VALIDATE-ACCOUNT.
           IF WS-ACCT-STATUS NOT = 'A'
               MOVE 'INACTIVE ACCOUNT' TO WS-ERROR-MSG
               GO TO 2199-VALIDATE-ACCOUNT-EXIT
           END-IF
           IF WS-ACCT-HOLD = 'Y'
               MOVE 'ACCOUNT ON HOLD' TO WS-ERROR-MSG
               GO TO 2199-VALIDATE-ACCOUNT-EXIT
           END-IF
           MOVE 'Y' TO WS-ACCT-VALID
           .
       2199-VALIDATE-ACCOUNT-EXIT.
           EXIT.

The EXIT statement in the exit paragraph does nothing — it is a no-op. It simply provides a target for the GO TO and a defined endpoint for the PERFORM THRU range.

⚖️ Shop Standards: Whether to allow PERFORM THRU is a shop-standard decision. Some large enterprises (particularly those with IBM's structured programming heritage) use it extensively. Others ban it. If your shop uses it, follow the convention. If starting a new project, consider whether the guard-clause-with-GO-TO pattern it enables is worth the risks. We will revisit this debate in Chapter 8.


7.9 EXIT PERFORM and EXIT PERFORM CYCLE (COBOL 2002+)

COBOL 2002 introduced two statements that give inline PERFORMs the ability to control loop flow without GO TO:

EXIT PERFORM — Break Out of a Loop

       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           IF WS-ACCT-NUM(WS-IDX) = WS-SEARCH-ACCT
               MOVE WS-IDX TO WS-FOUND-IDX
               EXIT PERFORM
           END-IF
       END-PERFORM

EXIT PERFORM immediately exits the innermost enclosing inline PERFORM loop. Execution continues with the statement after END-PERFORM. This is equivalent to break in C/Java.

EXIT PERFORM CYCLE — Skip to Next Iteration

       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           IF WS-STATUS(WS-IDX) = 'I'
               EXIT PERFORM CYCLE
           END-IF
           ADD WS-AMOUNT(WS-IDX) TO WS-TOTAL
           ADD 1 TO WS-ACTIVE-COUNT
       END-PERFORM

EXIT PERFORM CYCLE skips the rest of the current iteration and begins the next one (after incrementing the VARYING counter, if applicable). This is equivalent to continue in C/Java.

⚠️ Compatibility Note: EXIT PERFORM and EXIT PERFORM CYCLE are COBOL 2002 features. IBM Enterprise COBOL supports them from V4.2 onward. GnuCOBOL supports them. If your shop uses an older compiler, you may need to use condition flags or GO TO with PERFORM THRU instead.

Combining EXIT PERFORM with Search Logic

       MOVE 'N' TO WS-FOUND-FLAG
       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           EVALUATE TRUE
               WHEN WS-ACCT-NUM(WS-IDX) = WS-SEARCH-ACCT
                   MOVE 'Y' TO WS-FOUND-FLAG
                   MOVE WS-IDX TO WS-FOUND-IDX
                   EXIT PERFORM
               WHEN WS-ACCT-NUM(WS-IDX) > WS-SEARCH-ACCT
      *            Table is sorted; if we've passed it, stop
                   EXIT PERFORM
           END-EVALUATE
       END-PERFORM

7.10 Simulating Recursion in COBOL

COBOL does not natively support recursion (with limited exceptions in COBOL 2002+ for programs with the RECURSIVE attribute). However, some algorithms — tree traversal, expression parsing, directory walking — are naturally recursive. COBOL programmers simulate recursion using an explicit stack.

Stack-Based Recursion Pattern

       01  WS-STACK.
           05  WS-STACK-PTR         PIC 9(04) VALUE 0.
           05  WS-STACK-ENTRY OCCURS 100 TIMES.
               10  WS-STK-LEVEL     PIC 9(04).
               10  WS-STK-NODE-ID   PIC X(10).
               10  WS-STK-RETURN    PIC 9(04).

       01  WS-CURRENT-NODE          PIC X(10).
       01  WS-CURRENT-LEVEL         PIC 9(04).
      * Push onto stack
       3100-STACK-PUSH.
           ADD 1 TO WS-STACK-PTR
           IF WS-STACK-PTR > 100
               DISPLAY 'STACK OVERFLOW'
               PERFORM 9100-ABEND
           END-IF
           MOVE WS-CURRENT-LEVEL TO
               WS-STK-LEVEL(WS-STACK-PTR)
           MOVE WS-CURRENT-NODE TO
               WS-STK-NODE-ID(WS-STACK-PTR)
           .

      * Pop from stack
       3200-STACK-POP.
           IF WS-STACK-PTR < 1
               DISPLAY 'STACK UNDERFLOW'
               PERFORM 9100-ABEND
           END-IF
           MOVE WS-STK-LEVEL(WS-STACK-PTR) TO
               WS-CURRENT-LEVEL
           MOVE WS-STK-NODE-ID(WS-STACK-PTR) TO
               WS-CURRENT-NODE
           SUBTRACT 1 FROM WS-STACK-PTR
           .

      * Process tree using explicit stack
       3000-WALK-TREE.
           PERFORM 3100-STACK-PUSH
           PERFORM 3300-PROCESS-NODE
               UNTIL WS-STACK-PTR = 0
           .

       3300-PROCESS-NODE.
      *    Process current node
           DISPLAY 'Processing node: ' WS-CURRENT-NODE
               ' at level ' WS-CURRENT-LEVEL

      *    Push children onto stack (if any)
           PERFORM 3310-PUSH-CHILDREN

      *    Pop next node
           IF WS-STACK-PTR > 0
               PERFORM 3200-STACK-POP
           END-IF
           .

🔵 Industry Context: Stack-based recursion simulation is not just a textbook curiosity. At MedClaim, the diagnosis code hierarchy (ICD-10 codes) forms a tree structure. When James Okafor's team needed to traverse the hierarchy to find all child codes for a given parent code, they used exactly this stack-based pattern. The alternative — flattening the hierarchy into multiple sequential passes — would have been slower and harder to maintain.


7.11 Defensive Programming: Preventing Infinite Loops

An infinite loop in a batch program is a production emergency. The job consumes CPU time, holds file locks, prevents downstream jobs from running, and may require operator intervention to cancel. Defensive loop programming is essential.

Strategy 1: Maximum Iteration Counter

Always include a safety counter in loops that depend on external data:

       01  WS-MAX-ITERATIONS        PIC 9(09) VALUE 999999999.
       01  WS-ITERATION-COUNT       PIC 9(09) VALUE 0.

       PERFORM 2100-PROCESS-RECORD
           UNTIL END-OF-FILE
           OR WS-ITERATION-COUNT >= WS-MAX-ITERATIONS

       IF WS-ITERATION-COUNT >= WS-MAX-ITERATIONS
           DISPLAY 'WARNING: Maximum iterations reached'
           DISPLAY 'Records processed: ' WS-ITERATION-COUNT
           MOVE 16 TO RETURN-CODE
       END-IF

Inside the processing paragraph:

       2100-PROCESS-RECORD.
           ADD 1 TO WS-ITERATION-COUNT
           ...
           PERFORM 2110-READ-NEXT-RECORD
           .

🔴 Production War Story: In 2021, a junior developer at GlobalBank wrote a record-processing loop that depended on a flag being set inside a called subprogram. The subprogram had a bug that prevented the flag from being set under certain data conditions. The batch job ran for four hours, processing the same record 15 million times, before the operations team noticed the excessive CPU consumption and cancelled it. The fix was two lines: a safety counter and a check. Derek Washington now includes safety counters in every loop he writes, without exception.

Strategy 2: Progress Verification

For loops that modify data, verify that progress is being made:

       01  WS-LAST-KEY              PIC X(20).

       2100-PROCESS-RECORD.
           IF WS-CURRENT-KEY = WS-LAST-KEY
               ADD 1 TO WS-SAME-KEY-COUNT
               IF WS-SAME-KEY-COUNT > 10
                   DISPLAY 'ERROR: Loop not advancing'
                   DISPLAY 'Stuck on key: ' WS-CURRENT-KEY
                   MOVE 16 TO RETURN-CODE
                   SET END-OF-FILE TO TRUE
               END-IF
           ELSE
               MOVE 0 TO WS-SAME-KEY-COUNT
               MOVE WS-CURRENT-KEY TO WS-LAST-KEY
           END-IF
           ...
           .

Strategy 3: Checkpoint Logging

For long-running batch jobs, log progress at regular intervals:

       2100-PROCESS-RECORD.
           ADD 1 TO WS-RECORD-COUNT

           IF FUNCTION MOD(WS-RECORD-COUNT, 10000) = 0
               DISPLAY 'Checkpoint: '
                   WS-RECORD-COUNT ' records processed'
               DISPLAY 'Last key: ' WS-CURRENT-KEY
               DISPLAY 'Time: ' FUNCTION CURRENT-DATE
           END-IF
           ...
           .

This does not prevent infinite loops, but it makes them detectable. If the checkpoint log shows the same key or the same count repeatedly, the operations team knows something is wrong.

🧪 Try It Yourself: Defensive Loop

Write a loop that reads records from a sequential file and processes them, with all three defensive strategies in place: maximum iteration counter, progress verification, and checkpoint logging. Test it by deliberately creating a scenario where the end-of-file flag is never set.


7.12 Loop Optimization Patterns

In high-volume batch processing, loop efficiency matters. Here are patterns that can significantly improve performance.

Minimize Work Inside the Loop

Move anything that does not change per-iteration outside the loop:

      * BEFORE: Unnecessary work inside loop
       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           COMPUTE WS-TAX-RATE = WS-BASE-RATE * 1.07
           COMPUTE WS-TAX(WS-IDX) =
               WS-AMOUNT(WS-IDX) * WS-TAX-RATE
       END-PERFORM

      * AFTER: Compute constant outside loop
       COMPUTE WS-TAX-RATE = WS-BASE-RATE * 1.07
       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           COMPUTE WS-TAX(WS-IDX) =
               WS-AMOUNT(WS-IDX) * WS-TAX-RATE
       END-PERFORM

Use Index Names for Table Access

COBOL INDEX data items are more efficient than subscripts for table access because the compiler can use them directly as offsets:

       01  WS-RATE-TABLE.
           05  WS-RATE-ENTRY OCCURS 100 TIMES
               INDEXED BY WS-RATE-IDX.
               10  WS-RATE-CODE  PIC X(04).
               10  WS-RATE-AMT   PIC S9(05)V99.

      * Using INDEX (faster — index is a machine offset)
       SET WS-RATE-IDX TO 1
       SEARCH WS-RATE-ENTRY
           AT END
               MOVE 'NOT FOUND' TO WS-MSG
           WHEN WS-RATE-CODE(WS-RATE-IDX) = WS-SEARCH-CODE
               MOVE WS-RATE-AMT(WS-RATE-IDX) TO WS-RESULT
       END-SEARCH

We will cover SEARCH and INDEX in depth in Chapter 12 (Table Handling).

Early Exit from Search Loops

If you are searching a sorted table, exit as soon as you pass the target:

       MOVE 'N' TO WS-FOUND
       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > WS-TABLE-SIZE
           OR WS-CODE(WS-IDX) > WS-SEARCH-CODE
           IF WS-CODE(WS-IDX) = WS-SEARCH-CODE
               MOVE 'Y' TO WS-FOUND
           END-IF
       END-PERFORM

The OR WS-CODE(WS-IDX) > WS-SEARCH-CODE condition ensures the loop stops as soon as it passes the target value in a sorted table, rather than scanning the entire table.


7.13 Nested Loops: Multi-Level Processing Patterns

In real-world batch processing, you frequently need loops within loops. A claim has multiple line items; each line item may have multiple diagnosis codes; each diagnosis code may have multiple modifiers. Understanding nested loop patterns — and their pitfalls — is essential.

Pattern 1: Sequential Nested Loops

Process a header record, then process all its detail records before moving to the next header:

       2000-PROCESS-ALL-CLAIMS.
           PERFORM 2100-READ-CLAIM-HEADER
           PERFORM 2200-PROCESS-ONE-CLAIM
               UNTIL END-OF-CLAIMS
           .

       2200-PROCESS-ONE-CLAIM.
      *    Process the header
           PERFORM 2210-VALIDATE-HEADER
           IF HEADER-IS-VALID
      *        Process all line items for this claim
               PERFORM 2300-READ-LINE-ITEM
               PERFORM 2400-PROCESS-LINE-ITEM
                   UNTIL END-OF-LINE-ITEMS
                   OR WS-LINE-ERRORS > WS-MAX-LINE-ERRORS
               PERFORM 2500-FINALIZE-CLAIM
           END-IF
      *    Read next claim header
           PERFORM 2100-READ-CLAIM-HEADER
           .

The outer loop iterates over claims; the inner loop iterates over line items within each claim. Notice the critical details:

  1. Separate end-of-data flags: The inner loop uses END-OF-LINE-ITEMS, not END-OF-CLAIMS. These are separate flags that must be managed independently.

  2. Inner loop safety: The inner loop has an error limit (WS-MAX-LINE-ERRORS) to prevent one bad claim from consuming all processing time.

  3. Flag reset: The END-OF-LINE-ITEMS flag must be reset at the beginning of each claim, or it will be true from the previous claim and the inner loop will never execute.

       2200-PROCESS-ONE-CLAIM.
           SET END-OF-LINE-ITEMS TO FALSE    *> CRITICAL: Reset!
           MOVE 0 TO WS-LINE-ERRORS         *> Reset error counter
           ...

⚠️ Warning: Forgetting to reset inner-loop control variables between outer-loop iterations is one of the most common bugs in nested loop processing. James Okafor's MedClaim case study (Section 7.14) shows exactly this type of failure.

Pattern 2: Table of Tables

Processing a table where each entry contains a sub-table:

       01  WS-DEPARTMENT-TABLE.
           05  WS-DEPT-ENTRY OCCURS 10 TIMES.
               10  WS-DEPT-NAME         PIC X(20).
               10  WS-EMPLOYEE-COUNT    PIC 9(03).
               10  WS-EMPLOYEE OCCURS 50 TIMES.
                   15  WS-EMP-NAME      PIC X(20).
                   15  WS-EMP-SALARY    PIC S9(07)V99.

       01  WS-DEPT-IDX               PIC 9(02).
       01  WS-EMP-IDX                PIC 9(02).
       01  WS-DEPT-TOTAL             PIC S9(09)V99.
       01  WS-COMPANY-TOTAL          PIC S9(11)V99 VALUE 0.
      * Calculate salary totals by department
       PERFORM 2100-PROCESS-DEPARTMENT
           VARYING WS-DEPT-IDX FROM 1 BY 1
           UNTIL WS-DEPT-IDX > 10

       2100-PROCESS-DEPARTMENT.
           MOVE 0 TO WS-DEPT-TOTAL
           PERFORM 2200-PROCESS-EMPLOYEE
               VARYING WS-EMP-IDX FROM 1 BY 1
               UNTIL WS-EMP-IDX >
                   WS-EMPLOYEE-COUNT(WS-DEPT-IDX)
           DISPLAY 'Dept: '
               WS-DEPT-NAME(WS-DEPT-IDX)
               ' Total: ' WS-DEPT-TOTAL
           ADD WS-DEPT-TOTAL TO WS-COMPANY-TOTAL
           .

       2200-PROCESS-EMPLOYEE.
           ADD WS-EMP-SALARY(WS-DEPT-IDX, WS-EMP-IDX)
               TO WS-DEPT-TOTAL
           .

Note that 2200-PROCESS-EMPLOYEE uses both subscripts — the outer WS-DEPT-IDX and the inner WS-EMP-IDX. The inner paragraph must have access to the outer loop's counter. This is natural in COBOL because all data items are at the program level (there are no local variables within paragraphs), but it requires discipline to avoid accidentally modifying the outer counter.

Pattern 3: PERFORM VARYING with AFTER (Revisited)

For simple two-dimensional processing, PERFORM VARYING with AFTER is more concise than two separate PERFORM VARYING statements:

      * Using AFTER (concise)
       PERFORM 2100-INIT-CELL
           VARYING WS-ROW FROM 1 BY 1
           UNTIL WS-ROW > WS-MAX-ROWS
           AFTER WS-COL FROM 1 BY 1
           UNTIL WS-COL > WS-MAX-COLS

      * Equivalent without AFTER (more explicit but verbose)
       PERFORM 2050-PROCESS-ROW
           VARYING WS-ROW FROM 1 BY 1
           UNTIL WS-ROW > WS-MAX-ROWS
       ...
       2050-PROCESS-ROW.
           PERFORM 2100-INIT-CELL
               VARYING WS-COL FROM 1 BY 1
               UNTIL WS-COL > WS-MAX-COLS
           .

Both are correct. Use AFTER when the inner loop body is simple (one paragraph call). Use explicit nested PERFORM when the inner loop has additional setup, teardown, or control logic.

🧪 Try It Yourself: Nested Loop Processing

Write a program that manages a student gradebook. The data structure is a table of 5 courses, each with up to 20 students, each with up to 10 assignment grades. Use nested PERFORM VARYING to:

  1. Initialize all grades to zero
  2. Populate sample grades (use COMPUTE with a formula based on indices for variety)
  3. Calculate each student's average grade per course
  4. Calculate the class average for each course
  5. Find the overall highest-performing student across all courses

Pay special attention to resetting accumulators between loop iterations.


7.14 Loop Patterns for Report Generation

Report generation in COBOL frequently involves loops with accumulated totals, page breaks, and formatting. Here are common patterns:

Page Break Pattern

       01  WS-LINE-COUNT           PIC 9(03) VALUE 0.
       01  WS-PAGE-SIZE            PIC 9(03) VALUE 60.
       01  WS-PAGE-NUMBER          PIC 9(04) VALUE 0.

       2000-PROCESS-RECORD.
           IF WS-LINE-COUNT >= WS-PAGE-SIZE
               PERFORM 2500-PAGE-BREAK
           END-IF
           PERFORM 2100-WRITE-DETAIL-LINE
           ADD 1 TO WS-LINE-COUNT
           PERFORM 2050-READ-NEXT
           .

       2500-PAGE-BREAK.
           ADD 1 TO WS-PAGE-NUMBER
           PERFORM 2510-WRITE-PAGE-FOOTER
           PERFORM 2520-WRITE-PAGE-HEADER
           MOVE 0 TO WS-LINE-COUNT
           .

Running Total with Percentage

       01  WS-RUNNING-TOTAL        PIC S9(11)V99 VALUE 0.
       01  WS-GRAND-TOTAL          PIC S9(11)V99.
       01  WS-RUNNING-PCT          PIC 9(03)V99.

       2000-PROCESS-RECORD.
           ADD WS-AMOUNT TO WS-RUNNING-TOTAL
           IF WS-GRAND-TOTAL > 0
               COMPUTE WS-RUNNING-PCT =
                   (WS-RUNNING-TOTAL / WS-GRAND-TOTAL)
                   * 100
           ELSE
               MOVE 0 TO WS-RUNNING-PCT
           END-IF
           PERFORM 2100-WRITE-DETAIL-WITH-PCT
           PERFORM 2050-READ-NEXT
           .

Note the defensive check: IF WS-GRAND-TOTAL > 0 prevents a division by zero. This is the Defensive Programming theme in action — always guard against arithmetic exceptions inside loops.

Countdown Timer Pattern

For batch jobs with progress reporting:

       01  WS-START-TIME           PIC 9(08).
       01  WS-CURRENT-TIME         PIC 9(08).
       01  WS-ELAPSED-SECONDS      PIC 9(06).
       01  WS-RECORDS-PER-SECOND   PIC 9(07).

       2000-PROCESS-RECORD.
           ADD 1 TO WS-RECORD-COUNT

           IF FUNCTION MOD(WS-RECORD-COUNT, 100000) = 0
               ACCEPT WS-CURRENT-TIME FROM TIME
               COMPUTE WS-ELAPSED-SECONDS =
                   (WS-CURRENT-TIME - WS-START-TIME) / 100
               IF WS-ELAPSED-SECONDS > 0
                   COMPUTE WS-RECORDS-PER-SECOND =
                       WS-RECORD-COUNT / WS-ELAPSED-SECONDS
               END-IF
               DISPLAY 'Progress: '
                   WS-RECORD-COUNT ' records, '
                   WS-RECORDS-PER-SECOND ' rec/sec'
           END-IF
           ...
           .

This pattern helps operations staff monitor batch job performance and detect slowdowns before they become critical.


7.15 GlobalBank Case Study: Batch Transaction Processing

GlobalBank's nightly batch processes all transactions from the day. The main processing loop demonstrates several iteration patterns working together:

       01  WS-BATCH-STATS.
           05  WS-RECORDS-READ       PIC 9(09) VALUE 0.
           05  WS-RECORDS-PROCESSED  PIC 9(09) VALUE 0.
           05  WS-RECORDS-REJECTED   PIC 9(09) VALUE 0.
           05  WS-TOTAL-DEBITS       PIC S9(13)V99 VALUE 0.
           05  WS-TOTAL-CREDITS      PIC S9(13)V99 VALUE 0.
       01  WS-MAX-RECORDS            PIC 9(09) VALUE 5000000.
       01  WS-CHECKPOINT-INTERVAL    PIC 9(07) VALUE 10000.

       01  WS-FILE-STATUS            PIC X(02).
           88  FILE-OK               VALUE '00'.
           88  END-OF-FILE           VALUE '10'.
           88  FILE-ERROR            VALUE '30' THRU '99'.

       01  WS-LAST-ACCT-KEY         PIC X(10) VALUE SPACES.
       01  WS-STUCK-COUNT           PIC 9(05) VALUE 0.

       PROCEDURE DIVISION.
       1000-MAIN-PROCESS.
           PERFORM 1100-INITIALIZE
           PERFORM 1200-OPEN-FILES
           PERFORM 1300-PROCESS-ALL-TRANSACTIONS
           PERFORM 1400-WRITE-BATCH-TOTALS
           PERFORM 1500-CLOSE-FILES
           STOP RUN.

       1300-PROCESS-ALL-TRANSACTIONS.
      *    Priming read
           PERFORM 2100-READ-TXN-RECORD

      *    Main processing loop with safety counter
           PERFORM 2000-PROCESS-ONE-TXN
               UNTIL END-OF-FILE
               OR WS-RECORDS-READ >= WS-MAX-RECORDS
               OR FILE-ERROR

      *    Post-loop verification
           EVALUATE TRUE
               WHEN END-OF-FILE
                   DISPLAY 'Normal end of file reached'
               WHEN WS-RECORDS-READ >= WS-MAX-RECORDS
                   DISPLAY 'WARNING: Max records limit reached'
                   MOVE 4 TO RETURN-CODE
               WHEN FILE-ERROR
                   DISPLAY 'ERROR: File error, status='
                       WS-FILE-STATUS
                   MOVE 16 TO RETURN-CODE
           END-EVALUATE
           .

       2000-PROCESS-ONE-TXN.
      *    --- Progress verification ---
           IF WS-TXN-ACCT-KEY = WS-LAST-ACCT-KEY
               ADD 1 TO WS-STUCK-COUNT
               IF WS-STUCK-COUNT > 1000
                   DISPLAY 'ERROR: Loop appears stuck on '
                       WS-TXN-ACCT-KEY
                   MOVE 16 TO RETURN-CODE
                   SET END-OF-FILE TO TRUE
                   GO TO 2000-PROCESS-EXIT
               END-IF
           ELSE
               MOVE 0 TO WS-STUCK-COUNT
               MOVE WS-TXN-ACCT-KEY TO WS-LAST-ACCT-KEY
           END-IF

      *    --- Checkpoint logging ---
           IF FUNCTION MOD(WS-RECORDS-READ,
               WS-CHECKPOINT-INTERVAL) = 0
               DISPLAY 'Checkpoint: '
                   WS-RECORDS-READ ' read, '
                   WS-RECORDS-PROCESSED ' processed, '
                   WS-RECORDS-REJECTED ' rejected'
           END-IF

      *    --- Business logic ---
           PERFORM 2200-VALIDATE-TXN
           IF WS-TXN-VALID = 'Y'
               PERFORM 2300-APPLY-TXN
               ADD 1 TO WS-RECORDS-PROCESSED
           ELSE
               PERFORM 2400-WRITE-REJECT
               ADD 1 TO WS-RECORDS-REJECTED
           END-IF

      *    --- Read next ---
           PERFORM 2100-READ-TXN-RECORD
           .
       2000-PROCESS-EXIT.
           EXIT.

       2100-READ-TXN-RECORD.
           READ TXN-FILE INTO WS-TXN-RECORD
               AT END
                   SET END-OF-FILE TO TRUE
               NOT AT END
                   ADD 1 TO WS-RECORDS-READ
           END-READ
           .

This is production-quality batch processing code. Notice how it integrates multiple defensive patterns:

  1. Safety counter (WS-MAX-RECORDS) prevents runaway loops
  2. Progress verification (WS-STUCK-COUNT) detects loops stuck on one record
  3. Checkpoint logging at regular intervals for operations monitoring
  4. Post-loop verification distinguishes between normal and abnormal termination
  5. Priming read pattern with TEST BEFORE semantics

7.14 MedClaim Case Study: Iterating Through Claim Line Items

A single medical claim can contain multiple line items — each representing a different service, procedure, or supply. At MedClaim, the claim line item table is processed using nested PERFORM VARYING:

       01  WS-CLAIM-LINES.
           05  WS-LINE-COUNT         PIC 9(03) VALUE 0.
           05  WS-LINE-ENTRY OCCURS 99 TIMES.
               10  WS-LINE-NUM       PIC 9(03).
               10  WS-SVC-CODE       PIC X(05).
               10  WS-SVC-DATE       PIC 9(08).
               10  WS-BILLED-AMT     PIC S9(07)V99.
               10  WS-ALLOWED-AMT    PIC S9(07)V99.
               10  WS-COPAY-AMT      PIC S9(05)V99.
               10  WS-LINE-STATUS    PIC X(02).
                   88  LINE-APPROVED VALUE 'AP'.
                   88  LINE-DENIED   VALUE 'DN'.
                   88  LINE-PENDED   VALUE 'PD'.

       01  WS-LINE-IDX              PIC 9(03).
       01  WS-APPROVED-TOTAL        PIC S9(09)V99 VALUE 0.
       01  WS-DENIED-TOTAL          PIC S9(09)V99 VALUE 0.
       01  WS-COPAY-TOTAL           PIC S9(07)V99 VALUE 0.
       01  WS-ALL-LINES-DECIDED     PIC X(01).
           88  ALL-DECIDED           VALUE 'Y'.
           88  NOT-ALL-DECIDED       VALUE 'N'.

       4000-ADJUDICATE-ALL-LINES.
      *    Process each line item
           PERFORM 4100-ADJUDICATE-ONE-LINE
               VARYING WS-LINE-IDX FROM 1 BY 1
               UNTIL WS-LINE-IDX > WS-LINE-COUNT

      *    Accumulate totals
           MOVE 0 TO WS-APPROVED-TOTAL
                     WS-DENIED-TOTAL
                     WS-COPAY-TOTAL
           SET ALL-DECIDED TO TRUE

           PERFORM 4200-TALLY-LINE
               VARYING WS-LINE-IDX FROM 1 BY 1
               UNTIL WS-LINE-IDX > WS-LINE-COUNT
           .

       4100-ADJUDICATE-ONE-LINE.
      *    Apply adjudication rules to one line
           PERFORM 4110-CHECK-SERVICE-COVERAGE
           IF WS-SVC-COVERED = 'Y'
               PERFORM 4120-CALCULATE-ALLOWED
               PERFORM 4130-APPLY-COPAY
               PERFORM 4140-CHECK-DEDUCTIBLE
               SET LINE-APPROVED(WS-LINE-IDX) TO TRUE
           ELSE
               SET LINE-DENIED(WS-LINE-IDX) TO TRUE
               MOVE 0 TO WS-ALLOWED-AMT(WS-LINE-IDX)
           END-IF
           .

       4200-TALLY-LINE.
           EVALUATE TRUE
               WHEN LINE-APPROVED(WS-LINE-IDX)
                   ADD WS-ALLOWED-AMT(WS-LINE-IDX)
                       TO WS-APPROVED-TOTAL
                   ADD WS-COPAY-AMT(WS-LINE-IDX)
                       TO WS-COPAY-TOTAL
               WHEN LINE-DENIED(WS-LINE-IDX)
                   ADD WS-BILLED-AMT(WS-LINE-IDX)
                       TO WS-DENIED-TOTAL
               WHEN LINE-PENDED(WS-LINE-IDX)
                   SET NOT-ALL-DECIDED TO TRUE
           END-EVALUATE
           .

This example shows how PERFORM VARYING works naturally with OCCURS tables and 88-level condition names. The code reads clearly: adjudicate each line, then tally the results.

🔗 Cross-Reference: We will cover OCCURS tables and indexed access in much greater depth in Chapters 11 and 12. For now, note how PERFORM VARYING and the table subscript work together seamlessly.


7.15 Working Through a Complete Batch Program

Let us trace through the complete design and implementation of a batch processing loop, incorporating all the patterns from this chapter. This walkthrough shows how the individual concepts combine in a real program.

Scenario

MedClaim needs a program that processes a daily eligibility file from an employer group. The file contains employee records that may be adds (new enrollments), changes (status updates), or terminations. The program must:

  1. Read each record from the eligibility file
  2. Validate the record format and content
  3. Route to the appropriate processing paragraph based on action type
  4. Update the member master file
  5. Write an audit trail record for every action
  6. Write rejected records to an error file with reason codes
  7. Produce a batch summary at the end

The Loop Structure

       1300-PROCESS-ALL-ELIGIBILITY.
      *    Priming read
           PERFORM 2100-READ-ELIG-RECORD
      *    Main processing loop with defensive controls
           PERFORM 2000-PROCESS-ONE-RECORD
               UNTIL END-OF-FILE
               OR WS-RECORDS-READ > WS-MAX-RECORDS
               OR WS-FATAL-ERROR = 'Y'

      *    Post-loop diagnostics
           PERFORM 1310-VERIFY-COMPLETION
           .

       2000-PROCESS-ONE-RECORD.
      *    ---- Defensive: Safety counter ----
           ADD 1 TO WS-LOOP-ITERATIONS

      *    ---- Defensive: Progress check ----
           IF WS-ELIG-SSN = WS-LAST-SSN
               ADD 1 TO WS-SAME-KEY-COUNT
               IF WS-SAME-KEY-COUNT > WS-STUCK-THRESHOLD
                   MOVE 'LOOP STUCK ON SSN ' TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
                   MOVE 'Y' TO WS-FATAL-ERROR
                   GO TO 2000-PROCESS-EXIT
               END-IF
           ELSE
               MOVE 0 TO WS-SAME-KEY-COUNT
               MOVE WS-ELIG-SSN TO WS-LAST-SSN
           END-IF

      *    ---- Defensive: Checkpoint ----
           IF FUNCTION MOD(WS-RECORDS-READ,
               WS-CHECKPOINT-INTERVAL) = 0
               PERFORM 8100-WRITE-CHECKPOINT
           END-IF

      *    ---- Business logic ----
           PERFORM 2200-VALIDATE-RECORD
           IF RECORD-IS-VALID
               EVALUATE TRUE
                   WHEN ACTION-IS-ADD
                       PERFORM 3100-ADD-MEMBER
                   WHEN ACTION-IS-CHANGE
                       PERFORM 3200-CHANGE-MEMBER
                   WHEN ACTION-IS-TERMINATE
                       PERFORM 3300-TERMINATE-MEMBER
                   WHEN OTHER
                       MOVE 'UNKNOWN ACTION CODE'
                           TO WS-ERROR-MSG
                       PERFORM 9200-WRITE-REJECT
               END-EVALUATE
           ELSE
               PERFORM 9200-WRITE-REJECT
           END-IF

      *    ---- Unconditional: Audit and read next ----
           PERFORM 2300-WRITE-AUDIT
           PERFORM 2100-READ-ELIG-RECORD
           .
       2000-PROCESS-EXIT.
           EXIT.

Design Decisions Explained

Why priming read? We use TEST BEFORE (the default), so the first record must be available before the loop begins. This is the standard pattern from Section 7.4.

Why three loop-exit conditions? (1) END-OF-FILE is the normal termination. (2) WS-RECORDS-READ > WS-MAX-RECORDS is the safety counter from Section 7.11. (3) WS-FATAL-ERROR allows the business logic to signal an unrecoverable problem. Using OR in the UNTIL clause means any condition can stop the loop.

Why is the audit written unconditionally? Every record — valid or invalid, processed or rejected — gets an audit trail entry. This is a compliance requirement. The audit paragraph writes different information depending on the outcome but is always called.

The Validation Paragraph

       2200-VALIDATE-RECORD.
           SET RECORD-IS-VALID TO TRUE

      *    Check SSN is numeric
           IF WS-ELIG-SSN-X IS NOT NUMERIC
               MOVE 'SSN IS NOT NUMERIC' TO WS-ERROR-MSG
               SET RECORD-IS-INVALID TO TRUE
           END-IF

      *    Check name is alphabetic
           IF RECORD-IS-VALID
               IF WS-ELIG-NAME-X IS NOT ALPHABETIC
                   MOVE 'NAME CONTAINS INVALID CHARS'
                       TO WS-ERROR-MSG
                   SET RECORD-IS-INVALID TO TRUE
               END-IF
           END-IF

      *    Check action code is valid
           IF RECORD-IS-VALID
               IF NOT ACTION-CODE-VALID
                   MOVE 'INVALID ACTION CODE'
                       TO WS-ERROR-MSG
                   SET RECORD-IS-INVALID TO TRUE
               END-IF
           END-IF

      *    Check effective date is numeric
           IF RECORD-IS-VALID
               IF WS-ELIG-EFF-DATE-X IS NOT NUMERIC
                   MOVE 'EFF DATE IS NOT NUMERIC'
                       TO WS-ERROR-MSG
                   SET RECORD-IS-INVALID TO TRUE
               END-IF
           END-IF
           .

This validation paragraph demonstrates the cascading validation pattern: each check is only performed if all previous checks passed. This prevents cascading error messages and ensures that the first error detected is the one reported.

The Post-Loop Verification

       1310-VERIFY-COMPLETION.
           EVALUATE TRUE
               WHEN END-OF-FILE
                   DISPLAY 'Normal completion: '
                       WS-RECORDS-READ ' records processed'
                   MOVE 0 TO RETURN-CODE
               WHEN WS-RECORDS-READ > WS-MAX-RECORDS
                   DISPLAY 'WARNING: Safety limit reached at '
                       WS-RECORDS-READ ' records'
                   MOVE 8 TO RETURN-CODE
               WHEN WS-FATAL-ERROR = 'Y'
                   DISPLAY 'ABORTED: ' WS-ERROR-MSG
                   MOVE 16 TO RETURN-CODE
           END-EVALUATE

      *    Verify counts balance
           IF WS-PROCESSED-COUNT + WS-REJECTED-COUNT
              NOT = WS-RECORDS-READ
               DISPLAY 'WARNING: Record counts do not balance!'
               DISPLAY '  Read: ' WS-RECORDS-READ
               DISPLAY '  Processed: ' WS-PROCESSED-COUNT
               DISPLAY '  Rejected: ' WS-REJECTED-COUNT
               IF RETURN-CODE < 8
                   MOVE 8 TO RETURN-CODE
               END-IF
           END-IF
           .

This verification is critical. The counts-must-balance check catches bugs where records are silently dropped or double-counted. If the counts do not balance, something is wrong in the processing logic.

🧪 Try It Yourself: Complete Batch Program

Implement this eligibility processing program in your Student Mainframe Lab. Use a simulated data table (as in the DEFENSIVE-LOOP.cbl code example) instead of actual file I/O. Include at least 20 test records with a mix of adds, changes, terminations, and invalid records. Test the stuck-loop detection by including consecutive duplicate SSNs.


7.16 PERFORM and the COBOL Runtime Model

Understanding how PERFORM works at the runtime level helps you avoid subtle bugs and make better design decisions.

The PERFORM Stack

When COBOL executes a PERFORM statement, it places a return address on an internal stack. When the performed paragraph completes (its last statement executes, or it reaches the exit paragraph of a THRU range), execution returns to the statement after the PERFORM.

This has an important implication: PERFORM is not a GO TO. It remembers where to return. This is what makes structured programming possible in COBOL — PERFORM gives you subroutine-like behavior within a single program.

Nested PERFORM Depth

PERFORMs can be nested: paragraph A PERFORMs paragraph B, which PERFORMs paragraph C:

       1000-MAIN.
           PERFORM 2000-PROCESS          *> Push return to 1000
           ...

       2000-PROCESS.
           PERFORM 2100-VALIDATE         *> Push return to 2000
           PERFORM 2200-CALCULATE        *> Push return to 2000
           ...

       2100-VALIDATE.
           PERFORM 2110-CHECK-STATUS     *> Push return to 2100
           ...

The PERFORM stack grows with each nested call and shrinks as paragraphs complete. In theory, COBOL supports deep nesting. In practice, overly deep nesting (>10 levels) can be a sign that your program's decomposition needs rethinking.

The Active PERFORM Rule

COBOL has an important rule: you must not PERFORM a paragraph that is currently active (already on the PERFORM stack). For example:

      * DANGEROUS: Recursion via PERFORM
       2000-PROCESS.
           IF WS-LEVEL > 0
               SUBTRACT 1 FROM WS-LEVEL
               PERFORM 2000-PROCESS      *> Recursive PERFORM!
           END-IF
           .

This is technically undefined behavior in standard COBOL. Some compilers handle it gracefully; others produce unpredictable results. This is why simulating recursion requires an explicit stack (as discussed in Section 7.10) rather than recursive PERFORM.

⚠️ Exception: COBOL 2002 introduced the RECURSIVE attribute for programs called via CALL. This allows true recursion between programs, but not within a single program via PERFORM. Recursive PERFORM remains non-standard.

PERFORM and Paragraph Boundaries

A subtle but important rule: PERFORM transfers control to the first statement of the named paragraph and returns after the last statement of that paragraph (or the exit paragraph in a THRU range). The paragraph's terminal period is part of the paragraph. Consider:

       2100-VALIDATE.
           IF condition
               MOVE 'ERROR' TO WS-MSG
           END-IF
           .
       2200-CALCULATE.
           COMPUTE ...
           .

When you PERFORM 2100-VALIDATE, execution starts at the IF and returns after the period that follows END-IF. It does not fall through into 2200-CALCULATE. This is unlike regular sequential execution (where code flows from one paragraph to the next without PERFORM). Understanding this distinction is essential for avoiding bugs when mixing PERFORMed and sequentially executed paragraphs.

GO TO and the PERFORM Stack

When a GO TO within a PERFORMed paragraph jumps to a paragraph outside the PERFORM range, the PERFORM stack becomes corrupted. This is why GO TO should only jump to exit paragraphs within the same PERFORM THRU range — it ensures the PERFORM stack remains consistent.

      * SAFE: GO TO within PERFORM THRU range
       PERFORM 2100-VALIDATE THRU 2199-VALIDATE-EXIT
       ...
       2100-VALIDATE.
           IF error
               GO TO 2199-VALIDATE-EXIT   *> Within range — safe
           END-IF
           ...
       2199-VALIDATE-EXIT.
           EXIT.

      * DANGEROUS: GO TO outside PERFORM range
       PERFORM 2100-VALIDATE
       ...
       2100-VALIDATE.
           IF error
               GO TO 9100-ERROR           *> Outside range — unsafe!
           END-IF
           ...

The second example corrupts the PERFORM stack. Execution jumps to 9100-ERROR, but the PERFORM's return address is still on the stack. When 9100-ERROR completes, the program may return to an unexpected location or abend. This is the primary reason most coding standards restrict GO TO to within PERFORM THRU ranges.


7.17 PERFORM Timing and Efficiency

In high-volume batch processing, the overhead of PERFORM itself — the stack push/pop and branch — is worth understanding, even though it is typically negligible.

PERFORM Overhead

On IBM z/Architecture, a basic PERFORM (out-of-line, no VARYING) compiles to approximately 3-4 machine instructions: save return address, branch to target, and return. At modern mainframe clock speeds (5+ GHz), this takes roughly 1 nanosecond. Even in a loop processing 10 million records, the PERFORM overhead adds only about 10 milliseconds — completely negligible.

However, what you do inside the performed paragraph matters enormously. A paragraph that performs a VSAM random read (approximately 1-5 milliseconds per read) dwarfs any PERFORM overhead by a factor of a million.

Inline PERFORM vs. Out-of-Line PERFORM

Inline PERFORM avoids the branch overhead entirely because the loop body is inline. For extremely tight inner loops processing millions of iterations with minimal work per iteration, inline PERFORM can be measurably faster. For all other cases, the difference is undetectable.

      * Inline: no branch overhead (fastest for tight loops)
       PERFORM VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > 10000000
           ADD WS-VALUE(WS-IDX) TO WS-TOTAL
       END-PERFORM

      * Out-of-line: branch overhead (negligible for most cases)
       PERFORM 2100-ADD-VALUE
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > 10000000

Priya Kapoor's recommendation: "Use inline PERFORM for simple accumulations in tight loops. Use out-of-line PERFORM for everything else. Do not sacrifice readability for nanoseconds."


7.18 PERFORM VARYING: Detailed Execution Walkthrough

To cement your understanding of PERFORM VARYING, let us trace through the exact execution sequence, statement by statement. Understanding the order of operations — when the counter is initialized, when the condition is tested, when the counter is incremented — is essential for avoiding off-by-one errors and understanding counter values after the loop.

Execution Sequence for PERFORM VARYING

The COBOL standard defines this precise sequence for PERFORM para VARYING id FROM val1 BY val2 UNTIL condition:

  1. Set id to val1 (the FROM value)
  2. Test the UNTIL condition (TEST BEFORE is default)
  3. If condition is TRUE, exit the loop. id retains its current value.
  4. If condition is FALSE, execute the paragraph
  5. Add val2 to id (the BY value)
  6. Return to step 2

Let us trace through a specific example:

       PERFORM 2100-PROCESS
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > 3
Step WS-IDX Test: WS-IDX > 3? Action
1 1 Set WS-IDX to 1 (FROM)
2 1 1 > 3? NO
3 1 Execute 2100-PROCESS
4 2 Add 1 (BY) to WS-IDX
5 2 2 > 3? NO
6 2 Execute 2100-PROCESS
7 3 Add 1 to WS-IDX
8 3 3 > 3? NO
9 3 Execute 2100-PROCESS
10 4 Add 1 to WS-IDX
11 4 4 > 3? YES Exit loop

After the loop, WS-IDX = 4. The paragraph was executed 3 times (with WS-IDX values 1, 2, 3).

Execution Sequence for VARYING with AFTER

For PERFORM VARYING ... AFTER, the execution is more complex. The AFTER identifier completes its full range for each value of the VARYING identifier:

       PERFORM 2100-PROCESS
           VARYING WS-ROW FROM 1 BY 1
           UNTIL WS-ROW > 2
           AFTER WS-COL FROM 1 BY 1
           UNTIL WS-COL > 3
Iteration WS-ROW WS-COL Action
1 1 1 Execute 2100-PROCESS
2 1 2 Execute 2100-PROCESS
3 1 3 Execute 2100-PROCESS
1 4 WS-COL > 3: TRUE. Increment WS-ROW, reset WS-COL
4 2 1 Execute 2100-PROCESS
5 2 2 Execute 2100-PROCESS
6 2 3 Execute 2100-PROCESS
2 4 WS-COL > 3: TRUE. Increment WS-ROW, reset WS-COL
3 1 WS-ROW > 2: TRUE. Exit loop

The paragraph executed 6 times (2 rows times 3 columns). After the loop, WS-ROW = 3 and WS-COL = 1 (reset during the exit sequence).

TEST AFTER Execution Sequence

With TEST AFTER, the sequence changes:

  1. Set id to val1
  2. Execute the paragraph (always, at least once)
  3. Add val2 to id
  4. Test the UNTIL condition
  5. If condition is TRUE, exit. If FALSE, return to step 2.
       PERFORM 2100-PROCESS
           WITH TEST AFTER
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > 3
Step WS-IDX Action Test
1 1 Set WS-IDX to 1
2 1 Execute 2100-PROCESS
3 2 Add 1 to WS-IDX 2 > 3? NO
4 2 Execute 2100-PROCESS
5 3 Add 1 to WS-IDX 3 > 3? NO
6 3 Execute 2100-PROCESS
7 4 Add 1 to WS-IDX 4 > 3? YES, exit

Result: 3 executions, WS-IDX = 4 after loop. Same result as TEST BEFORE in this case — but if the FROM value were 4 (already past the limit), TEST BEFORE would execute 0 times while TEST AFTER would execute 1 time.


7.19 Common Pitfalls

Pitfall 1: Modifying the Loop Counter Inside the Loop

      * DANGEROUS: Modifying WS-IDX inside the loop
       PERFORM 2100-PROCESS
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > 100
       ...
       2100-PROCESS.
           IF WS-CONDITION
               SUBTRACT 1 FROM WS-IDX    *> DON'T DO THIS
           END-IF
           .

Modifying the VARYING counter inside the performed paragraph produces unpredictable behavior and can cause infinite loops. If you need to skip elements or reprocess, use a different control mechanism.

Pitfall 2: Missing the Priming Read

      * BUG: No priming read — first record is never processed
       PERFORM 2100-READ-AND-PROCESS
           UNTIL END-OF-FILE
       ...
       2100-READ-AND-PROCESS.
           READ INPUT-FILE ...
           PERFORM 2200-PROCESS-RECORD
           .

With the read inside the loop, the first iteration reads a record and processes it — but the UNTIL condition is only checked between iterations. When the last record is read, it is processed, and then the next READ triggers the end-of-file. With TEST BEFORE, the loop exits before trying to process a nonexistent record. But the very first record was read and processed without being checked first. The correct pattern depends on whether you use TEST BEFORE or TEST AFTER.

Pitfall 3: Off-by-One in PERFORM VARYING

      * Processes elements 1 through 99 (NOT 100!)
       PERFORM 2100-PROCESS
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX = 100

      * Correct: processes elements 1 through 100
       PERFORM 2100-PROCESS
           VARYING WS-IDX FROM 1 BY 1
           UNTIL WS-IDX > 100

The first version stops when WS-IDX = 100, so element 100 is never processed. Use > rather than = to avoid this off-by-one error.

Pitfall 4: PERFORM THRU with Intervening Paragraphs

       PERFORM 2100-START THRU 2199-EXIT

       2100-START.
           ... original logic ...
           .
       2150-NEW-PARAGRAPH.
           ... added later by another programmer ...
           .
       2199-EXIT.
           EXIT.

If someone adds 2150-NEW-PARAGRAPH between the start and exit paragraphs, it will be executed as part of the THRU range — even if that was not intended. This is why some shops ban PERFORM THRU.


7.16 Putting It All Together: Iteration Patterns Reference

Pattern Use Case Key Consideration
PERFORM para Single execution of a logical unit Foundation of top-down design
PERFORM para N TIMES Fixed repetition Cannot exit early
PERFORM para UNTIL cond Condition-controlled loop Default is TEST BEFORE (0+ executions)
PERFORM para WITH TEST AFTER UNTIL At-least-once loop Always executes at least once
PERFORM para VARYING ... UNTIL Counting/table traversal Counter value after loop is beyond range
PERFORM para VARYING ... AFTER Nested counting (2D tables) AFTER varies faster than VARYING
PERFORM ... END-PERFORM (inline) Short, local loop body Keep under ~25 lines
PERFORM para THRU exit Guard clause / early exit Controversial; shop-standard dependent
EXIT PERFORM Break from inline loop COBOL 2002+ only
EXIT PERFORM CYCLE Skip to next iteration COBOL 2002+ only

7.21 Iteration and the Mainframe Batch Window

Understanding iteration patterns is not just a technical skill — it is an operational skill. COBOL batch programs do not run in isolation. They run within a batch window — a scheduled period (typically overnight) when the mainframe processes the day's accumulated work.

The Batch Window Constraint

At GlobalBank, the batch window runs from 10 PM to 6 AM — eight hours. Within this window, approximately 50 batch jobs must complete in sequence, each depending on the output of the previous one:

10:00 PM - Job 1: Extract daily transactions from CICS logs
10:30 PM - Job 2: Sort transactions by account number
10:45 PM - Job 3: Process transactions (TXN-PROC) — 45 min
11:30 PM - Job 4: Update account master file
12:00 AM - Job 5: Calculate interest for all accounts
12:30 AM - Job 6: Generate regulatory reports (CTR, SAR)
 1:00 AM - Job 7: Process ACH/wire transfers
 ...
 5:30 AM - Job 48: Generate customer statements
 6:00 AM - Online system restart

If any single job runs long — because of an infinite loop, a performance degradation, or an unexpected data volume spike — every downstream job is delayed. If the batch window overruns, the online system cannot restart on time, and 2.3 million customers cannot access their accounts.

This is why the defensive programming patterns from this chapter are not optional. They are operational necessities:

  • Safety counters prevent infinite loops from consuming the entire batch window
  • Checkpoint logging allows operations staff to monitor progress and detect problems early
  • Progress verification catches stuck loops before they waste hours of processing time
  • Post-loop verification ensures that the job completed correctly and sets appropriate return codes for downstream job scheduling

Derek Washington's insight after his first year at GlobalBank: "In school, a loop that runs too long just makes your program slow. On the mainframe, a loop that runs too long makes the entire bank slow. When 2.3 million people cannot check their balances because your loop did not terminate, your loop is not just a bug — it is a business continuity incident."

Iteration Performance at Scale

To put the numbers in perspective: GlobalBank's TXN-PROC processes 2.3 million transactions in 45 minutes. That is approximately 51,000 transactions per minute, or 850 per second. Each transaction involves: - Reading a record (1 PERFORM + 1 READ) - Validating it (1 PERFORM + ~5 condition checks) - Searching the account table (1 PERFORM + ~17 binary search comparisons) - Applying the transaction (1 PERFORM + 2-3 computations) - Writing audit records (1 PERFORM + 1 WRITE)

That is approximately 30 PERFORMs and 25 condition evaluations per transaction, totaling roughly 70 million PERFORMs and 57 million condition evaluations per night. At these volumes, the patterns in this chapter — how you structure your loops, where you place your condition checks, whether you use binary search versus linear search — have measurable impact on whether the batch window succeeds or fails.


Chapter Summary

This chapter has taken you through every form of COBOL's PERFORM statement — from the basic single-execution PERFORM to complex nested VARYING loops with AFTER clauses. You have learned the critical difference between TEST BEFORE and TEST AFTER, mastered the priming read pattern for sequential file processing, and explored both sides of the PERFORM THRU debate.

Most importantly, you have learned to program defensively against infinite loops — the most dangerous bug in batch processing. Safety counters, progress verification, and checkpoint logging are not luxuries. They are the safety nets that keep batch jobs running reliably and maintainably.

As James Okafor tells his team at MedClaim: "Every loop terminates. If you cannot prove that your loop terminates for all possible inputs, you do not understand your loop well enough to put it into production."

In Chapter 8, we will zoom out from individual statements to program design — how paragraphs and sections organize your conditional logic and iteration patterns into programs that are coherent, maintainable, and professionally structured.


Key Terms

Term Definition
Priming read An initial READ performed before a TEST BEFORE loop to establish the first record
TEST BEFORE Loop condition is checked before each iteration; may execute zero times (default)
TEST AFTER Loop condition is checked after each iteration; always executes at least once
PERFORM VARYING A counting loop that automatically initializes, increments, and tests a counter
AFTER clause Adds an inner loop counter to PERFORM VARYING for nested iteration
Inline PERFORM Loop body written between PERFORM and END-PERFORM rather than in a separate paragraph
PERFORM THRU Executes a range of consecutive paragraphs
EXIT PERFORM Immediately exits the innermost inline PERFORM loop (COBOL 2002+)
EXIT PERFORM CYCLE Skips to the next iteration of an inline PERFORM loop (COBOL 2002+)
Safety counter A maximum iteration count that prevents infinite loops in batch processing
Checkpoint logging Periodic progress messages during batch processing for monitoring and debugging