COBOL predates structured programming as a formal discipline. The original 1959/1960 specifications provided no scope terminators, GO TO was the primary branching mechanism, and paragraph fall-through was routine. Dijkstra's 1968 letter exposed these patterns as error-prone; COBOL responded slowly — COBOL-74 added PERFORM ... UNTIL, COBOL-85 added explicit scope terminators (END-IF, END-PERFORM, etc.), and COBOL 2002 added object-orientation.
The practical consequence: most active COBOL codebases span multiple standards. A program written in 1975, extended in 1992, and patched in 2008 will mix styles. Structured programming in COBOL is therefore partly a retrofit discipline — you need to recognise unstructured idioms and know how to contain or replace them.
Every COBOL program is partitioned into exactly four divisions, in fixed order. Structured programming begins here — the division layout enforces separation of declaration from logic.
| Division | Purpose | Key Sections |
|---|---|---|
| IDENTIFICATION | Program identity metadata | PROGRAM-ID, AUTHOR |
| ENVIRONMENT | File and I/O hardware binding | CONFIGURATION, INPUT-OUTPUT |
| DATA | All variable and record declarations | WORKING-STORAGE, LOCAL-STORAGE, FILE, LINKAGE |
| PROCEDURE | Executable logic — paragraphs and sections | User-defined |
WORKING-STORAGE vs LOCAL-STORAGE. WORKING-STORAGE is initialized once at program load and persists across calls. LOCAL-STORAGE (COBOL-85+) is re-initialized on every call to the program — equivalent to stack-allocated locals in C. Subprograms that must be reentrant should use LOCAL-STORAGE for mutable state.
*> WORKING-STORAGE: lives for the lifetime of the run unit
WORKING-STORAGE SECTION.
01 WS-RECORD-COUNT PIC 9(7) VALUE 0.
01 WS-STATUS-FLAG PIC X(2) VALUE '00'.
*> LOCAL-STORAGE: re-initialized on each CALL
LOCAL-STORAGE SECTION.
01 LS-LOOP-INDEX PIC 9(4) VALUE 0.
COBOL
Sections within the PROCEDURE DIVISION group paragraphs. In structured COBOL, sections are either avoided entirely (everything is paragraphs) or used purely as namespace containers — never relied upon for fall-through execution between sections.
A paragraph is the basic unit of named executable code in COBOL. It begins with a name in columns 8–11 (Area A) followed by a period, and ends at the next paragraph name or division/section header.
PROCEDURE DIVISION.
0000-MAIN.
PERFORM 1000-INITIALIZE
PERFORM 2000-PROCESS-RECORDS
PERFORM 3000-FINALIZE
STOP RUN.
1000-INITIALIZE.
OPEN INPUT CUSTOMER-FILE
OPEN OUTPUT REPORT-FILE
MOVE 0 TO WS-RECORD-COUNT
MOVE '00' TO WS-STATUS-FLAG.
2000-PROCESS-RECORDS.
READ CUSTOMER-FILE
AT END MOVE 'Y' TO WS-EOF-FLAG
END-READ
PERFORM UNTIL WS-EOF-FLAG = 'Y'
PERFORM 2100-PROCESS-ONE-RECORD
READ CUSTOMER-FILE
AT END MOVE 'Y' TO WS-EOF-FLAG
END-READ
END-PERFORM.
2100-PROCESS-ONE-RECORD.
ADD 1 TO WS-RECORD-COUNT
PERFORM 2110-VALIDATE-RECORD
IF WS-STATUS-FLAG = 'OK'
PERFORM 2120-WRITE-REPORT-LINE
END-IF.
2110-VALIDATE-RECORD.
MOVE 'OK' TO WS-STATUS-FLAG
IF CUST-AMOUNT < 0
MOVE 'ER' TO WS-STATUS-FLAG
END-IF.
COBOL
The numeric prefix hierarchy is industry-standard and structurally important. The leading number communicates call depth and module ownership at a glance:
| Prefix | Level | Role |
|---|---|---|
| 0000- | Main | Entry point, top-level orchestration only |
| 1000–3000- | Major modules | Initialize / Process / Finalize phases |
| x100–x900- | Sub-modules | Logical sub-tasks of a major module |
| x110–x190- | Utilities | Single-purpose helpers called from multiple parents |
| 9000- | Error handling | Shared error/abort routines |
PERFORM is the structured replacement for GO TO. It transfers control to a named paragraph and returns automatically when that paragraph reaches its terminal period. This is the mechanism that makes COBOL paragraphs function as subroutines.
*> 1. Simple call — execute once and return
PERFORM 2100-PROCESS-ONE-RECORD
*> 2. Inline PERFORM with scope terminator (COBOL-85+)
PERFORM
MOVE SPACES TO WS-BUFFER
MOVE 0 TO WS-COUNT
END-PERFORM
*> 3. Fixed iteration
PERFORM 2100-PROCESS-ONE-RECORD 10 TIMES
*> 4. Conditional loop — test-before (default)
PERFORM UNTIL WS-EOF-FLAG = 'Y'
PERFORM 2100-PROCESS-ONE-RECORD
END-PERFORM
*> 5. Test-after loop (executes body at least once)
PERFORM WITH TEST AFTER UNTIL WS-RETRY-COUNT > 3
PERFORM 5000-ATTEMPT-CONNECT
ADD 1 TO WS-RETRY-COUNT
END-PERFORM
*> 6. VARYING — indexed loop with optional AFTER clause
PERFORM VARYING WS-IDX FROM 1 BY 1
UNTIL WS-IDX > WS-TABLE-SIZE
PERFORM 4100-PROCESS-TABLE-ENTRY
END-PERFORM
COBOL
PERFORM para-A THRU para-B executes all paragraphs from A through B in source order. This creates implicit coupling between paragraph order and program behavior — reordering paragraphs changes semantics. Avoid it in new code. When maintaining legacy code that uses it, never insert a paragraph between A and B without auditing all PERFORM THRU ranges that include that span.
PERFORM THRU that references a section name executes the entire section. This is a common source of unintended execution in legacy programs where sections were added for organisational reasons without considering PERFORM ranges.
Before COBOL-85, IF was terminated by a period. A period anywhere inside a nested IF silently ended the entire conditional, regardless of indentation. This is the source of a large class of bugs in pre-85 COBOL. Always use END-IF.
*> Pre-85 style — period terminates ALL open IFs
*> Do NOT write new code this way
IF WS-FLAG = 'Y'
IF WS-AMOUNT > 1000
PERFORM 5000-HIGH-VALUE
ELSE
PERFORM 5100-LOW-VALUE. *< period closes BOTH IFs
*> COBOL-85+: END-IF makes nesting unambiguous
IF WS-FLAG = 'Y'
IF WS-AMOUNT > 1000
PERFORM 5000-HIGH-VALUE
ELSE
PERFORM 5100-LOW-VALUE
END-IF
ELSE
PERFORM 5200-FLAG-NOT-SET
END-IF
COBOL
COBOL supports AND, OR, NOT, and abbreviated combined relation conditions. Abbreviated conditions are a readability trap for the uninitiated:
*> Full form — unambiguous
IF WS-CODE = 'A' OR WS-CODE = 'B' OR WS-CODE = 'C'
*> Abbreviated form — equivalent, but confusing to readers
IF WS-CODE = 'A' OR 'B' OR 'C'
*> Class conditions
IF WS-INPUT IS NUMERIC
PERFORM 4000-NUMERIC-BRANCH
END-IF
*> Sign conditions
IF WS-BALANCE IS NEGATIVE
PERFORM 9100-OVERDRAFT-ERROR
END-IF
*> 88-level condition names (preferred for flag testing)
01 WS-EOF-FLAG PIC X.
88 WS-EOF VALUE 'Y'.
88 WS-NOT-EOF VALUE 'N'.
*> Usage
PERFORM UNTIL WS-EOF
PERFORM 2100-PROCESS-ONE-RECORD
END-PERFORM
COBOL
88-level items are the idiomatic way to name boolean states. SET WS-EOF TO TRUE assigns the VALUE literal to the parent field. They eliminate magic literals scattered throughout condition tests.
EVALUATE (COBOL-85+) is a generalized case statement. It is the correct replacement for deeply nested IF/ELSE chains and for any GO TO ... DEPENDING ON construct. It evaluates one or more subjects against one or more conditions per WHEN clause and executes the first matching branch.
EVALUATE WS-TRANSACTION-CODE
WHEN 'ADD'
PERFORM 3100-ADD-RECORD
WHEN 'UPD'
PERFORM 3200-UPDATE-RECORD
WHEN 'DEL'
PERFORM 3300-DELETE-RECORD
WHEN OTHER
PERFORM 9000-INVALID-CODE-ERROR
END-EVALUATE
COBOL
EVALUATE TRUE matches the first WHEN clause whose condition is true. This pattern cleanly replaces nested IF chains where each branch has a different condition:
EVALUATE TRUE
WHEN WS-AMOUNT > 100000
PERFORM 5100-PLATINUM-TIER
WHEN WS-AMOUNT > 10000
PERFORM 5200-GOLD-TIER
WHEN WS-AMOUNT > 1000
PERFORM 5300-SILVER-TIER
WHEN OTHER
PERFORM 5400-STANDARD-TIER
END-EVALUATE
COBOL
Multiple subjects and multiple conditions per WHEN clause are matched positionally. ANY is a wildcard that matches any value in that position:
EVALUATE WS-REGION ALSO WS-PRODUCT-CLASS
WHEN 'NORTH' ALSO 'A'
PERFORM 6100-NORTH-CLASS-A
WHEN 'NORTH' ALSO ANY
PERFORM 6110-NORTH-DEFAULT
WHEN 'SOUTH' ALSO ANY
PERFORM 6200-SOUTH-DEFAULT
WHEN OTHER
PERFORM 9200-REGION-ERROR
END-EVALUATE
COBOL
WHEN clauses are tested top-to-bottom and execution stops at the first match. There is no fall-through between WHEN clauses in COBOL — no equivalent of C's break is needed.
Scope terminators were introduced in COBOL-85. Every compound verb that can contain nested logic has a corresponding terminator. Use them consistently — mixing period-terminated and terminator-terminated forms within the same program creates ambiguity and defeats static analysis tools.
| Verb | Scope Terminator | Notes |
|---|---|---|
| IF | END-IF | Required for unambiguous nested conditionals |
| EVALUATE | END-EVALUATE | Required; no implicit fall-through |
| PERFORM | END-PERFORM | Required for inline PERFORM blocks |
| READ | END-READ | Scopes AT END / NOT AT END / INVALID KEY |
| WRITE | END-WRITE | Scopes INVALID KEY / NOT INVALID KEY |
| REWRITE | END-REWRITE | Same as WRITE |
| DELETE | END-DELETE | VSAM/indexed only |
| START | END-START | VSAM/indexed only |
| CALL | END-CALL | Scopes ON EXCEPTION / NOT ON EXCEPTION |
| COMPUTE | END-COMPUTE | Scopes ON SIZE ERROR |
| ADD / SUBTRACT / MULTIPLY / DIVIDE | END-ADD etc. | Scopes ON SIZE ERROR |
| STRING / UNSTRING | END-STRING / END-UNSTRING | Scopes ON OVERFLOW |
| ACCEPT | END-ACCEPT | IBM extension; not in all compilers |
| SEARCH | END-SEARCH | Scopes AT END and WHEN clauses |
COBOL's fixed-format layout (Area A columns 8–11, Area B columns 12–72) was designed for 80-column punch cards. Structured programs use indentation within these constraints to communicate nesting depth. The compiler ignores indentation — it is entirely for human readers.
*> Columns: 1234567890123456789012345678...
*> ----A---+----B---+----B---+--...
*> (8) (12)
2200-CALCULATE-PREMIUM. *< Area A: col 8
EVALUATE WS-RISK-CLASS *< Area B: col 12
WHEN 'HIGH' *< indent 4 per level
COMPUTE WS-PREMIUM =
WS-BASE-RATE * 2.5
END-COMPUTE
WHEN 'MED'
COMPUTE WS-PREMIUM =
WS-BASE-RATE * 1.5
END-COMPUTE
WHEN OTHER
MOVE WS-BASE-RATE TO WS-PREMIUM
END-EVALUATE.
COBOL
Paragraph termination. The period that ends a paragraph is functionally significant — it terminates all open scopes at that point. Standard practice is to place the terminal period on its own line after the last statement, or at the end of the last statement. Never place a period mid-paragraph except as part of a literal.
IF or PERFORM block silently closes all containing scopes. Most compilers issue no diagnostic. This is a primary source of logical errors when editing legacy code or reformatting with automated tools.
A practical limit of 3–4 levels of nested conditionals per paragraph keeps logic comprehensible. If you reach 5 or more levels, extract the inner logic into a called paragraph. Deep nesting is the most reliable signal that a paragraph has too many responsibilities.
GO TO transfers control to a named paragraph without a return mechanism. In structured COBOL it has two legitimate uses: early exit from a paragraph, and GO TO ... DEPENDING ON (though the latter is replaced by EVALUATE in new code).
COBOL has no RETURN statement. The idiomatic way to exit a paragraph early is to GO TO its own exit point — a convention supported by the EXIT verb:
2110-VALIDATE-RECORD.
MOVE 'OK' TO WS-STATUS-FLAG
IF CUST-ID = SPACES
MOVE 'ER' TO WS-STATUS-FLAG
GO TO 2110-EXIT
END-IF
IF CUST-AMOUNT IS NOT NUMERIC
MOVE 'ER' TO WS-STATUS-FLAG
GO TO 2110-EXIT
END-IF
*> Further validation only reached if prior checks pass
PERFORM 2111-RANGE-CHECK.
2110-EXIT.
EXIT.
COBOL
EXIT is a no-op statement; it exists solely to provide a named target. The paragraph para-EXIT must immediately follow the working paragraph in source order for this pattern to be safe — it is the one case where paragraph order matters.
PERFORM with multiple sentences and inline logic that can reduce the need for this pattern. GnuCOBOL 3.x supports it as well. In COBOL 2002+, EVALUATE TRUE chains frequently eliminate the need for early exit entirely.
PERFORM (no return)| Anti-Pattern | Problem | Structured Replacement |
|---|---|---|
| ALTER verb | Dynamically modifies a GO TO target at runtime. Makes control flow impossible to trace statically. |
EVALUATE or PERFORM with condition variable |
| GO TO outside exit pattern | Unrestricted jumps produce spaghetti; no return mechanism | PERFORM for all calls; GO TO para-EXIT for early exit only |
| PERFORM THRU with intervening paragraphs | Execution depends on source order; reordering breaks program | Replace with explicit individual PERFORM calls |
| Period inside nested IF | Silently closes all open scopes; compiler gives no warning | Use END-IF consistently; terminal period only at paragraph end |
| Paragraph fall-through | Execution continues into the next paragraph after the period; appears intentional but is error-prone | Every paragraph ends with explicit transfer: PERFORM, GO TO para-EXIT, or STOP RUN |
| Deeply nested IF chains (5+ levels) | Reduces maintainability; increases risk of scope errors | EVALUATE TRUE or extract logic into sub-paragraphs |
| Magic literals in conditions | IF WS-FLAG = 'Y' — meaning of 'Y' is implicit |
Define 88-level condition names on the data item |
| GO TO DEPENDING ON | Computed branch target; no compile-time check on valid range | EVALUATE with explicit WHEN clauses and WHEN OTHER |
Applied during code review or before committing modifications to a legacy program:
IF is closed by END-IF, not a paragraph-terminal periodEVALUATE has a WHEN OTHER clause covering unmatched casesPERFORM loop has a reachable termination condition (no possible infinite loop on valid input)ALTER verb anywhere in the compilation unitGO TO that targets a paragraph other than the current paragraph's -EXITPERFORM THRU that spans more than the working paragraph and its exit stubAT END, INVALID KEY, ON EXCEPTION)WORKING-STORAGE items are initialized via VALUE clause or explicit MOVE at program start — never relied upon as implicitly zero/spaceON SIZE ERROR where overflow is possible