> "In COBOL, PERFORM is not merely a loop — it is the fundamental mechanism of program control."
In This Chapter
- 7.1 The PERFORM Family — An Overview
- 7.2 Basic PERFORM — The Building Block
- 7.3 PERFORM ... TIMES — Fixed Repetition
- 7.4 PERFORM ... UNTIL — Condition-Controlled Loops
- 7.5 PERFORM ... VARYING — The Counting Loop
- 7.6 PERFORM VARYING with AFTER — Nested Counting
- 7.7 Inline PERFORM vs. Out-of-Line PERFORM
- 7.8 PERFORM THRU — Range Execution
- 7.9 EXIT PERFORM and EXIT PERFORM CYCLE (COBOL 2002+)
- 7.10 Simulating Recursion in COBOL
- 7.11 Defensive Programming: Preventing Infinite Loops
- 7.12 Loop Optimization Patterns
- 7.13 Nested Loops: Multi-Level Processing Patterns
- 7.14 Loop Patterns for Report Generation
- 7.15 GlobalBank Case Study: Batch Transaction Processing
- 7.14 MedClaim Case Study: Iterating Through Claim Line Items
- 7.15 Working Through a Complete Batch Program
- 7.16 PERFORM and the COBOL Runtime Model
- 7.17 PERFORM Timing and Efficiency
- 7.18 PERFORM VARYING: Detailed Execution Walkthrough
- 7.19 Common Pitfalls
- 7.16 Putting It All Together: Iteration Patterns Reference
- 7.21 Iteration and the Mainframe Batch Window
- Chapter Summary
- Key Terms
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:
-
Separate end-of-data flags: The inner loop uses
END-OF-LINE-ITEMS, notEND-OF-CLAIMS. These are separate flags that must be managed independently. -
Inner loop safety: The inner loop has an error limit (
WS-MAX-LINE-ERRORS) to prevent one bad claim from consuming all processing time. -
Flag reset: The
END-OF-LINE-ITEMSflag 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:
- Initialize all grades to zero
- Populate sample grades (use COMPUTE with a formula based on indices for variety)
- Calculate each student's average grade per course
- Calculate the class average for each course
- 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:
- Safety counter (
WS-MAX-RECORDS) prevents runaway loops - Progress verification (
WS-STUCK-COUNT) detects loops stuck on one record - Checkpoint logging at regular intervals for operations monitoring
- Post-loop verification distinguishes between normal and abnormal termination
- 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:
- Read each record from the eligibility file
- Validate the record format and content
- Route to the appropriate processing paragraph based on action type
- Update the member master file
- Write an audit trail record for every action
- Write rejected records to an error file with reason codes
- 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:
- Set
idtoval1(the FROM value) - Test the UNTIL condition (TEST BEFORE is default)
- If condition is TRUE, exit the loop.
idretains its current value. - If condition is FALSE, execute the paragraph
- Add
val2toid(the BY value) - 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:
- Set
idtoval1 - Execute the paragraph (always, at least once)
- Add
val2toid - Test the UNTIL condition
- 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 |