> "The interface between two programs is a contract. The parameter passing mode is the fine print. Get it wrong, and both sides suffer — usually silently." — Priya Kapoor, GlobalBank Systems Architect
In This Chapter
- 23.1 The Three Parameter Passing Modes
- 23.2 BY REFERENCE — Shared Storage
- 23.3 BY CONTENT — Copy Protection
- 23.4 BY VALUE — Non-COBOL Interoperability
- 23.5 Comparing the Three Modes — A Side-by-Side Example
- 23.6 OMITTED Parameters — Optional Arguments
- 23.6 The Length Fields Pattern
- 23.7 Return Code Conventions
- 23.8 Error Structure Pattern
- 23.9 Communication Areas — The COMMAREA Concept
- 23.10 The RETURN-CODE Special Register and Job-Level Communication
- 23.11 GlobalBank Case Study: Standardized Error Handling
- 23.11 MedClaim Case Study: Adjudication Engine Parameters
- 23.13 Defensive Parameter Validation Patterns
- 23.14 Try It Yourself: Safe Parameter Passing
- 23.13 The RETURNING Clause in Detail
- 23.14 Designing Parameter Blocks — Practical Patterns
- 23.15 Advanced Topic: Passing Tables and Complex Structures
- 23.16 GnuCOBOL Parameter Passing Notes
- 23.17 Common Pitfalls with Parameter Passing
- 23.18 Try It Yourself: Building an Error-Reporting Validation Module
- 23.19 Chapter Summary
Chapter 23: Parameter Passing Patterns
"The interface between two programs is a contract. The parameter passing mode is the fine print. Get it wrong, and both sides suffer — usually silently." — Priya Kapoor, GlobalBank Systems Architect
In the previous chapter, you learned the mechanics of CALL and subprogram linkage. You saw parameters passed on the USING clause and received in the LINKAGE SECTION. But we glossed over a critical detail: how those parameters are passed. The three parameter passing modes — BY REFERENCE, BY CONTENT, and BY VALUE — give you fine-grained control over data sharing, data protection, and cross-language interoperability.
This chapter is about mastering that fine print. We will examine each passing mode in depth, explore patterns for optional parameters and variable-length data, and establish the conventions for return codes and error communication that make large COBOL systems manageable. By the end, you will be able to design subprogram interfaces that are safe, efficient, and maintainable.
23.1 The Three Parameter Passing Modes
COBOL provides three ways to pass a parameter:
| Mode | What is Passed | Called Program Can Modify Caller's Data? | Primary Use |
|---|---|---|---|
| BY REFERENCE | Address of the data item | Yes | Default; bidirectional communication |
| BY CONTENT | Address of a copy of the data item | No (only the copy is modified) | Protecting caller's data |
| BY VALUE | The actual value (not an address) | No | Interop with C and other languages |
The default mode, if you do not specify one, is BY REFERENCE. This is the most common mode in pure COBOL systems. Let us examine each in detail.
23.2 BY REFERENCE — Shared Storage
When you pass a parameter BY REFERENCE, the calling program passes the address of the data item. The called program's LINKAGE SECTION item points directly to the caller's storage. Any modification the called program makes is immediately visible to the caller.
* Calling program
CALL 'SUBPROG' USING BY REFERENCE WS-BALANCE
This is equivalent to the default:
* These two calls are identical
CALL 'SUBPROG' USING WS-BALANCE
CALL 'SUBPROG' USING BY REFERENCE WS-BALANCE
How BY REFERENCE Works Internally
┌─────────────────────────┐
│ Calling Program Memory │
│ │
│ WS-BALANCE: 1500.00 │ ◄─── Address: 0x00A1B000
│ │
└─────────────────────────┘
│
│ Address passed
▼
┌─────────────────────────┐
│ Called Program │
│ │
│ LS-BALANCE ─────────────┼──► Points to 0x00A1B000
│ (LINKAGE SECTION) │ (caller's storage)
│ │
│ MOVE 2000.00 │
│ TO LS-BALANCE │ ──► Modifies caller's
│ │ WS-BALANCE directly!
└─────────────────────────┘
When to Use BY REFERENCE
BY REFERENCE is the right choice when:
- The called program needs to return data through the parameter — output parameters, updated records, computed results.
- Bidirectional communication — the parameter carries input data to the subprogram and the subprogram modifies it to carry output data back.
- Performance matters — passing an address is faster than copying data, especially for large structures.
BY REFERENCE Dangers
Because BY REFERENCE provides shared access to the caller's storage, it comes with risks:
* Calling program
01 WS-CUSTOMER-RECORD.
05 WS-CUST-ID PIC X(10) VALUE '1234567890'.
05 WS-CUST-NAME PIC X(30) VALUE 'JOHN SMITH'.
05 WS-CUST-STATUS PIC X(1) VALUE 'A'.
CALL 'CUSTPROC' USING WS-CUSTOMER-RECORD
* After the call, ANY field in WS-CUSTOMER-RECORD
* may have been changed by CUSTPROC — even fields
* that were intended as input-only!
There is no way, within COBOL syntax, to declare that a BY REFERENCE parameter is "input only." The called program can modify any byte of the parameter. This is why careful documentation and coding discipline are essential.
💡 Practitioner's Insight: "At GlobalBank, we document every parameter in the copybook with a comment indicating its direction: (I) for input, (O) for output, (I/O) for bidirectional. It's not enforced by the compiler, but it sets expectations for both the caller and the callee." — Maria Chen
A Complete BY REFERENCE Example
Here is a subprogram that calculates compound interest and modifies the account balance in place:
IDENTIFICATION DIVISION.
PROGRAM-ID. INTCALC.
*================================================================
* Interest Calculation Subprogram
* Calculates compound interest and updates the balance.
*
* Parameters (all BY REFERENCE — default):
* 1. LS-BALANCE (I/O) Current balance, updated with interest
* 2. LS-RATE (I) Annual interest rate (e.g., 0.045)
* 3. LS-PERIODS (I) Number of compounding periods
* 4. LS-INT-EARNED (O) Interest amount earned
* 5. LS-RETURN-CODE (O) Return code
*================================================================
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-ORIGINAL-BAL PIC S9(11)V99 COMP-3.
01 WS-PERIOD-RATE PIC S9(3)V9(8) COMP-3.
01 WS-PERIOD-IDX PIC S9(4) COMP.
LINKAGE SECTION.
01 LS-BALANCE PIC S9(11)V99 COMP-3.
01 LS-RATE PIC S9(3)V9(6) COMP-3.
01 LS-PERIODS PIC S9(4) COMP.
01 LS-INT-EARNED PIC S9(11)V99 COMP-3.
01 LS-RETURN-CODE PIC S9(4) COMP.
PROCEDURE DIVISION USING LS-BALANCE
LS-RATE
LS-PERIODS
LS-INT-EARNED
LS-RETURN-CODE.
CALC-INTEREST.
MOVE 0 TO LS-RETURN-CODE
MOVE 0 TO LS-INT-EARNED
* Validate inputs
IF LS-BALANCE < 0
MOVE 8 TO LS-RETURN-CODE
GOBACK
END-IF
IF LS-RATE < 0 OR LS-RATE > 1
MOVE 8 TO LS-RETURN-CODE
GOBACK
END-IF
IF LS-PERIODS < 1 OR LS-PERIODS > 365
MOVE 8 TO LS-RETURN-CODE
GOBACK
END-IF
* Save original balance
MOVE LS-BALANCE TO WS-ORIGINAL-BAL
* Calculate period rate
DIVIDE LS-RATE BY LS-PERIODS
GIVING WS-PERIOD-RATE
* Compound interest calculation
PERFORM VARYING WS-PERIOD-IDX FROM 1 BY 1
UNTIL WS-PERIOD-IDX > LS-PERIODS
COMPUTE LS-BALANCE ROUNDED =
LS-BALANCE * (1 + WS-PERIOD-RATE)
END-PERFORM
* Calculate interest earned
COMPUTE LS-INT-EARNED =
LS-BALANCE - WS-ORIGINAL-BAL
GOBACK.
Notice how LS-BALANCE serves as both input (the original balance) and output (the updated balance with interest). LS-RATE and LS-PERIODS are input-only by convention, though the compiler does not enforce this. LS-INT-EARNED and LS-RETURN-CODE are output-only.
BY REFERENCE with Group Items
When you pass a group item BY REFERENCE, the entire group — all its subordinate fields — is accessible to the called program through a single address:
* Calling program
01 WS-EMPLOYEE-RECORD.
05 WS-EMP-ID PIC X(8).
05 WS-EMP-NAME PIC X(30).
05 WS-EMP-DEPT PIC X(4).
05 WS-EMP-SALARY PIC S9(7)V99 COMP-3.
05 WS-EMP-HIRE-DATE PIC 9(8).
CALL 'EMPPROC' USING WS-EMPLOYEE-RECORD
WS-RETURN-CODE
* Called program
LINKAGE SECTION.
01 LS-EMPLOYEE-RECORD.
05 LS-EMP-ID PIC X(8).
05 LS-EMP-NAME PIC X(30).
05 LS-EMP-DEPT PIC X(4).
05 LS-EMP-SALARY PIC S9(7)V99 COMP-3.
05 LS-EMP-HIRE-DATE PIC 9(8).
The called program can access any field within the group — LS-EMP-ID, LS-EMP-NAME, LS-EMP-SALARY, etc. — because the LINKAGE SECTION describes the layout of the contiguous block of storage starting at the address that was passed.
This is why copybooks are so important. If the calling program uses one layout and the called program uses a different layout, the field offsets will not align, and the called program will read incorrect data from the wrong byte positions within the group.
Performance Characteristics of BY REFERENCE
BY REFERENCE is the fastest parameter passing mode because it involves no data copying. The runtime builds a parameter address list containing one address per parameter, which is a fixed overhead regardless of the parameter's size:
Passing a PIC X(10) BY REFERENCE:
Overhead: build address entry (~2 microseconds)
Data copy: none
Passing a PIC X(10000) BY REFERENCE:
Overhead: build address entry (~2 microseconds)
Data copy: none (same as PIC X(10)!)
This makes BY REFERENCE particularly efficient for large structures. Whether you pass a 10-byte field or a 10,000-byte record, the overhead is identical — only one address is passed. The called program accesses the data directly at its original storage location.
23.3 BY CONTENT — Copy Protection
When you pass a parameter BY CONTENT, the runtime creates a temporary copy of the data item and passes the address of that copy to the called program. The called program can modify the copy, but the caller's original data is protected.
* Calling program
CALL 'SUBPROG' USING BY CONTENT WS-ACCOUNT-ID
BY REFERENCE WS-RESULT
How BY CONTENT Works Internally
┌─────────────────────────┐
│ Calling Program Memory │
│ │
│ WS-ACCOUNT-ID: ABC123 │ ◄── Address: 0x00A1B000
│ │ │
└─────────────────────────┘ │ Copy made
▼
┌─────────────────────────┐ ┌──────────────┐
│ Called Program │ │ Temp Copy │
│ │ │ ABC123 │ ◄── Address: 0x00A2C000
│ LS-ACCOUNT-ID ──────────┼─►│ │
│ (LINKAGE SECTION) │ └──────────────┘
│ │
│ MOVE 'XYZ999' │ ──► Modifies the copy,
│ TO LS-ACCOUNT-ID │ NOT caller's original!
└─────────────────────────┘
After the call returns, the caller's WS-ACCOUNT-ID still contains 'ABC123'. The modification was made to the temporary copy, which is discarded.
When to Use BY CONTENT
BY CONTENT is the right choice when:
- You want to protect input data — the called program cannot accidentally modify data the caller depends on.
- You're passing literals or expressions — BY CONTENT allows you to pass literal values and arithmetic expressions.
- You want read-only semantics — even though COBOL cannot declare a parameter as "const," BY CONTENT achieves the same effect.
Passing Literals with BY CONTENT
One of the most useful features of BY CONTENT is the ability to pass literal values:
* Pass a literal string
CALL 'LOGMSG' USING BY CONTENT 'PROCESS STARTED'
BY REFERENCE WS-RETURN-CODE
* Pass a literal number
CALL 'SETLIMIT' USING BY CONTENT 1000
BY REFERENCE WS-RETURN-CODE
* Pass the LENGTH OF a data item
CALL 'FORMATTER' USING BY REFERENCE WS-DATA-BUFFER
BY CONTENT LENGTH OF
WS-DATA-BUFFER
BY REFERENCE WS-RETURN-CODE
⚠️ Important: When passing a literal BY CONTENT, the runtime creates a temporary data item to hold the literal value. The called program receives the address of this temporary item, so the LINKAGE SECTION declaration must be compatible with the literal's type and size.
BY CONTENT vs. Moving to a Temporary
Some developers achieve similar protection by copying data to a temporary item before the call:
* Manual protection — equivalent to BY CONTENT
MOVE WS-ACCOUNT-ID TO WS-TEMP-ACCT-ID
CALL 'SUBPROG' USING WS-TEMP-ACCT-ID
WS-RESULT
* Simpler with BY CONTENT
CALL 'SUBPROG' USING BY CONTENT WS-ACCOUNT-ID
BY REFERENCE WS-RESULT
BY CONTENT is cleaner and makes the intent explicit. It also avoids the need for temporary variables in WORKING-STORAGE.
Performance Characteristics of BY CONTENT
BY CONTENT incurs a copy overhead proportional to the parameter's size:
Passing a PIC X(10) BY CONTENT:
Overhead: build address entry + copy 10 bytes (~3 microseconds)
Passing a PIC X(10000) BY CONTENT:
Overhead: build address entry + copy 10,000 bytes (~10 microseconds)
For small parameters (under 100 bytes), the copy overhead is negligible. For large structures (thousands of bytes), consider whether the protection is worth the cost. In a loop that executes millions of times, the difference between BY REFERENCE and BY CONTENT for a 5KB structure could be measurable.
📊 Practical Guidance: At GlobalBank, Maria Chen's team uses BY CONTENT for parameters up to 500 bytes without hesitation. For parameters between 500 and 2,000 bytes, they evaluate whether the protection is needed (input-only parameters that must be preserved). For parameters over 2,000 bytes, they use BY REFERENCE and rely on coding discipline to treat input parameters as read-only.
BY CONTENT and the COPY-on-CALL Principle
You can think of BY CONTENT as implementing a "copy-on-call" policy. The runtime creates a copy at the moment of the CALL, and the copy exists only for the duration of that call. When GOBACK executes, the copy is discarded. This has an important implication: the called program cannot retain a reference to a BY CONTENT parameter's storage after GOBACK, because that storage may be reclaimed.
This is rarely a practical concern in traditional COBOL, but it matters in advanced scenarios involving pointer manipulation or Language Environment callable services.
Mixed Passing Modes
You can mix passing modes on a single CALL statement:
CALL 'ACCTLKUP' USING BY CONTENT WS-ACCOUNT-ID
BY CONTENT WS-LOOKUP-TYPE
BY REFERENCE WS-ACCOUNT-RECORD
BY REFERENCE WS-RETURN-CODE
This pattern clearly communicates intent: - WS-ACCOUNT-ID and WS-LOOKUP-TYPE are inputs (protected by BY CONTENT) - WS-ACCOUNT-RECORD and WS-RETURN-CODE are outputs (BY REFERENCE for modification)
💡 Best Practice: Using BY CONTENT for input parameters and BY REFERENCE for output parameters makes the data flow direction visible at the CALL site, even without reading the subprogram's documentation.
23.4 BY VALUE — Non-COBOL Interoperability
BY VALUE passes the actual value of the data item, not an address. This is the standard calling convention for languages like C, and it is essential for COBOL-to-C (or C-to-COBOL) interoperability.
CALL 'c_function' USING BY VALUE WS-INTEGER
BY REFERENCE WS-BUFFER
How BY VALUE Works Internally
┌─────────────────────────┐
│ Calling Program Memory │
│ │
│ WS-INTEGER: 42 │
│ │
└─────────────────────────┘
│
│ Value 42 pushed onto call stack
│ (not an address!)
▼
┌─────────────────────────┐
│ Called Program (C) │
│ │
│ int param = 42 │ ◄── Receives the value
│ │ directly, not a pointer
└─────────────────────────┘
Restrictions on BY VALUE
BY VALUE has significant restrictions in COBOL:
-
Data types: Only elementary items of certain types can be passed BY VALUE: - Binary integers (PIC S9(n) COMP, COMP-4, COMP-5, BINARY) - Floating-point (COMP-1, COMP-2) - Single-byte alphanumeric (PIC X) - Pointer data items (USAGE POINTER)
-
No group items: You cannot pass a group item BY VALUE.
-
Size limits: The item must fit in a register (typically 1, 2, 4, or 8 bytes).
-
Receiving side: The called program must also declare the corresponding parameter with BY VALUE in the PROCEDURE DIVISION USING clause:
* Called program receiving BY VALUE
PROCEDURE DIVISION USING BY VALUE LS-INTEGER
BY REFERENCE LS-BUFFER.
COBOL Calling C — A Complete Example
Here is a realistic example of COBOL calling a C function:
C function (strlen_safe.c):
#include <string.h>
/*
* Safe string length function for COBOL callers.
* Receives a pointer to a buffer and its maximum length.
* Returns the actual length (excluding trailing spaces).
*/
int strlen_safe(char *buffer, int max_len) {
int actual_len = max_len;
while (actual_len > 0 && buffer[actual_len - 1] == ' ') {
actual_len--;
}
return actual_len;
}
COBOL caller:
IDENTIFICATION DIVISION.
PROGRAM-ID. CINTEROP.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-BUFFER PIC X(100)
VALUE 'HELLO WORLD '.
01 WS-MAX-LEN PIC S9(9) COMP-5 VALUE 100.
01 WS-ACTUAL-LEN PIC S9(9) COMP-5.
PROCEDURE DIVISION.
MAIN-LOGIC.
* Call C function: int strlen_safe(char*, int)
* C expects: pointer (BY REFERENCE) + value (BY VALUE)
* C returns: int (captured via RETURNING)
CALL 'strlen_safe'
USING BY REFERENCE WS-BUFFER
BY VALUE WS-MAX-LEN
RETURNING WS-ACTUAL-LEN
END-CALL
DISPLAY 'Buffer: [' WS-BUFFER ']'
DISPLAY 'Actual length: ' WS-ACTUAL-LEN
STOP RUN.
The RETURNING Clause
The RETURNING clause captures a function's return value. It is used primarily with non-COBOL functions that return a value (like C functions that return int or double):
CALL 'c_function' USING BY VALUE WS-INPUT
RETURNING WS-RESULT
In the called COBOL program, the RETURNING clause is declared on the PROCEDURE DIVISION:
PROCEDURE DIVISION USING BY VALUE LS-INPUT
RETURNING LS-RESULT.
📊 Usage Statistics: In a survey of Enterprise COBOL shops, BY REFERENCE accounts for approximately 92% of all parameter passing, BY CONTENT for 6%, and BY VALUE for 2%. BY VALUE usage is almost exclusively for C/Java interoperability.
23.5 Comparing the Three Modes — A Side-by-Side Example
To make the differences concrete, here is a single scenario implemented with each passing mode. Imagine a subprogram that formats an account number — adding dashes and validating the format.
BY REFERENCE Version
* Caller
01 WS-ACCT-ID PIC X(10) VALUE '1234567890'.
...
CALL 'FMTACCT' USING WS-ACCT-ID WS-RC
* After: WS-ACCT-ID = '123-456-7890' (MODIFIED!)
The caller's WS-ACCT-ID is modified in place. This is efficient (no copy) but dangerous — the original unformatted value is lost.
BY CONTENT Version
* Caller
01 WS-ACCT-ID PIC X(10) VALUE '1234567890'.
01 WS-FORMATTED PIC X(13).
...
CALL 'FMTACCT' USING BY CONTENT WS-ACCT-ID
BY REFERENCE WS-FORMATTED
BY REFERENCE WS-RC
* After: WS-ACCT-ID still = '1234567890' (PRESERVED)
* WS-FORMATTED = '123-456-7890'
The caller's original value is preserved. The subprogram receives a copy to read from and writes the formatted result to a separate output parameter.
BY VALUE Version
* Not applicable here — BY VALUE cannot pass PIC X(10)
* (exceeds register size for BY VALUE)
BY VALUE is not suitable for this scenario because the data item is a 10-byte alphanumeric, which exceeds the size limits for BY VALUE. This illustrates why BY VALUE is primarily used for interop with C rather than for general COBOL-to-COBOL communication.
The Decision Matrix
| Consideration | BY REFERENCE | BY CONTENT | BY VALUE |
|---|---|---|---|
| Data protection | None — caller's data exposed | Caller's data protected | Caller's data protected |
| Performance | Best — no copy | Small overhead (copy) | Minimal (register) |
| Supported types | Any data item | Any data item | Elementary only, limited sizes |
| Direction | Bidirectional | Input only | Input only |
| Literal passing | Not supported | Supported | Supported (limited) |
| Primary use | Output/I-O params | Input params | C interop |
| Most common | ~92% of all params | ~6% | ~2% |
💡 Practitioner's Insight: "When I review code and see all parameters passed BY REFERENCE, I ask: which ones are inputs and which are outputs? If the developer cannot answer immediately by looking at the CALL statement, I suggest using BY CONTENT for inputs. The two extra words make the data flow direction self-documenting." — Priya Kapoor
23.6 OMITTED Parameters — Optional Arguments
COBOL allows you to omit parameters by specifying the OMITTED keyword in place of a data item:
CALL 'FORMATTER' USING WS-INPUT-DATA
WS-OUTPUT-DATA
OMITTED
WS-RETURN-CODE
When a parameter is OMITTED, the called program receives a null address for that position. The called program must check for this condition before accessing the parameter.
Checking for OMITTED Parameters
In Enterprise COBOL, you can use an address check:
LINKAGE SECTION.
01 LS-INPUT-DATA PIC X(100).
01 LS-OUTPUT-DATA PIC X(200).
01 LS-OPTIONS PIC X(50).
01 LS-RETURN-CODE PIC S9(4) COMP.
PROCEDURE DIVISION USING LS-INPUT-DATA
LS-OUTPUT-DATA
LS-OPTIONS
LS-RETURN-CODE.
MAIN-LOGIC.
* Check if optional parameters were provided
IF ADDRESS OF LS-OPTIONS = NULL
PERFORM USE-DEFAULT-OPTIONS
ELSE
PERFORM APPLY-OPTIONS
END-IF
...
A Pattern for Optional Parameters
Here is a robust pattern that James Okafor uses at MedClaim:
IDENTIFICATION DIVISION.
PROGRAM-ID. CLMFMT.
*================================================================
* Claim Formatting Subprogram
* Parameters:
* 1. LS-CLAIM-DATA (I) Required - claim record
* 2. LS-FORMAT-OUTPUT (O) Required - formatted output
* 3. LS-FORMAT-OPTIONS (I) Optional - formatting options
* 4. LS-RETURN-CODE (O) Required - return code
*
* If LS-FORMAT-OPTIONS is OMITTED, defaults are used:
* - Output format: STANDARD
* - Date format: MM/DD/YYYY
* - Amount format: $Z,ZZZ,ZZ9.99
*================================================================
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-DEFAULT-OPTIONS.
05 WS-DFL-OUTPUT-FMT PIC X(10) VALUE 'STANDARD'.
05 WS-DFL-DATE-FMT PIC X(10) VALUE 'MM/DD/YYYY'.
05 WS-DFL-AMT-FMT PIC X(15) VALUE '$Z,ZZZ,ZZ9.99'.
01 WS-ACTIVE-OPTIONS.
05 WS-ACT-OUTPUT-FMT PIC X(10).
05 WS-ACT-DATE-FMT PIC X(10).
05 WS-ACT-AMT-FMT PIC X(15).
LINKAGE SECTION.
01 LS-CLAIM-DATA PIC X(500).
01 LS-FORMAT-OUTPUT PIC X(1000).
01 LS-FORMAT-OPTIONS.
05 LS-OPT-OUTPUT-FMT PIC X(10).
05 LS-OPT-DATE-FMT PIC X(10).
05 LS-OPT-AMT-FMT PIC X(15).
01 LS-RETURN-CODE PIC S9(4) COMP.
PROCEDURE DIVISION USING LS-CLAIM-DATA
LS-FORMAT-OUTPUT
LS-FORMAT-OPTIONS
LS-RETURN-CODE.
FORMAT-CLAIM.
MOVE 0 TO LS-RETURN-CODE
* Determine active options
IF ADDRESS OF LS-FORMAT-OPTIONS = NULL
* Use defaults
MOVE WS-DFL-OUTPUT-FMT TO WS-ACT-OUTPUT-FMT
MOVE WS-DFL-DATE-FMT TO WS-ACT-DATE-FMT
MOVE WS-DFL-AMT-FMT TO WS-ACT-AMT-FMT
ELSE
* Use provided options, defaulting blanks
IF LS-OPT-OUTPUT-FMT = SPACES
MOVE WS-DFL-OUTPUT-FMT TO WS-ACT-OUTPUT-FMT
ELSE
MOVE LS-OPT-OUTPUT-FMT TO WS-ACT-OUTPUT-FMT
END-IF
IF LS-OPT-DATE-FMT = SPACES
MOVE WS-DFL-DATE-FMT TO WS-ACT-DATE-FMT
ELSE
MOVE LS-OPT-DATE-FMT TO WS-ACT-DATE-FMT
END-IF
IF LS-OPT-AMT-FMT = SPACES
MOVE WS-DFL-AMT-FMT TO WS-ACT-AMT-FMT
ELSE
MOVE LS-OPT-AMT-FMT TO WS-ACT-AMT-FMT
END-IF
END-IF
* Perform formatting with active options
PERFORM FORMAT-WITH-OPTIONS
GOBACK.
FORMAT-WITH-OPTIONS.
* (Formatting logic using WS-ACTIVE-OPTIONS)
CONTINUE.
Calling with options:
CALL 'CLMFMT' USING WS-CLAIM-DATA
WS-FORMAT-OUTPUT
WS-MY-OPTIONS
WS-RETURN-CODE
Calling without options (use defaults):
CALL 'CLMFMT' USING WS-CLAIM-DATA
WS-FORMAT-OUTPUT
OMITTED
WS-RETURN-CODE
23.6 The Length Fields Pattern
When passing variable-length data, you need a way to tell the called program how much data is actually present. The length fields pattern solves this:
*================================================================
* BUFFERCP — Variable-length buffer interface
*================================================================
01 BUFFER-INTERFACE.
05 BUF-MAX-LENGTH PIC S9(4) COMP.
05 BUF-ACTUAL-LENGTH PIC S9(4) COMP.
05 BUF-DATA PIC X(1).
* Note: BUF-DATA is declared as PIC X(1) but the
* actual data area is BUF-MAX-LENGTH bytes.
* The called program uses reference modification
* to access BUF-DATA(1:BUF-ACTUAL-LENGTH).
A more common and safer approach uses a fixed maximum with an actual length indicator:
*================================================================
* MSGBUFCP — Message buffer with length tracking
*================================================================
01 MSG-BUFFER-AREA.
05 MSG-MAX-SIZE PIC S9(4) COMP VALUE 500.
05 MSG-ACTUAL-SIZE PIC S9(4) COMP VALUE 0.
05 MSG-DATA PIC X(500).
The called program checks MSG-ACTUAL-SIZE before reading, and sets it after writing:
PROCEDURE DIVISION USING MSG-BUFFER-AREA
LS-RETURN-CODE.
PROCESS-MESSAGE.
* Validate buffer
IF MSG-ACTUAL-SIZE < 0 OR
MSG-ACTUAL-SIZE > MSG-MAX-SIZE
MOVE 8 TO LS-RETURN-CODE
GOBACK
END-IF
* Process only the actual data
EVALUATE MSG-DATA(1:3)
WHEN 'HDR'
PERFORM PROCESS-HEADER
WHEN 'DTL'
PERFORM PROCESS-DETAIL
WHEN OTHER
MOVE 8 TO LS-RETURN-CODE
END-EVALUATE
GOBACK.
Passing the LENGTH OF Special Register
COBOL's LENGTH OF special register provides the byte length of any data item. You can pass it BY CONTENT:
CALL 'VALIDATOR' USING BY REFERENCE WS-DATA-AREA
BY CONTENT LENGTH OF
WS-DATA-AREA
BY REFERENCE WS-RETURN-CODE
This is a common pattern for validation routines that need to know the size of the data they are processing.
23.7 Return Code Conventions
Return codes are the primary mechanism for subprograms to communicate success, failure, and severity to their callers. The IBM-standard convention, widely adopted across mainframe shops, is:
| Return Code | Meaning | Action |
|---|---|---|
| 0 | Success | Continue normal processing |
| 4 | Warning | Result valid but notable condition exists |
| 8 | Error | Result invalid, skip this item, continue batch |
| 12 | Severe error | Cannot continue current function |
| 16 | Fatal error | Cannot continue processing at all |
Why Multiples of 4?
The convention of using multiples of 4 dates back to early IBM assembler programming, where return codes were used as branch table offsets. Each entry in a branch table occupied 4 bytes (one branch instruction), so a return code of 0 branched to the first entry, 4 to the second, 8 to the third, and so on:
* Assembler branch table pattern (historical context)
B RC0_HANDLER Offset 0
B RC4_HANDLER Offset 4
B RC8_HANDLER Offset 8
B RC12_HANDLER Offset 12
In COBOL, we do not use branch tables, but the convention persists because it is universally understood in the mainframe world.
Implementing Return Code Handling
Here is the pattern GlobalBank uses across all modules:
*================================================================
* RETCODCP — Standard Return Code Interface
*================================================================
01 RTN-CODE-AREA.
05 RTN-CODE PIC S9(4) COMP.
88 RTN-SUCCESS VALUE 0.
88 RTN-WARNING VALUE 4.
88 RTN-ERROR VALUE 8.
88 RTN-SEVERE VALUE 12.
88 RTN-FATAL VALUE 16.
88 RTN-OK VALUE 0 THRU 4.
88 RTN-FAILED VALUE 8 THRU 16.
The RTN-OK and RTN-FAILED condition names are particularly useful:
CALL 'ACCTVAL' USING ACCT-INTERFACE-AREA
RTN-CODE-AREA
IF RTN-OK
PERFORM CONTINUE-PROCESSING
ELSE
PERFORM HANDLE-ERROR
END-IF
Propagating Return Codes
In a multi-level call chain, the highest (most severe) return code should propagate upward:
WORKING-STORAGE SECTION.
01 WS-MAX-RETURN-CODE PIC S9(4) COMP VALUE 0.
PROCEDURE DIVISION.
PROCESS-ALL.
CALL 'STEP1' USING WS-DATA RTN-CODE-AREA
IF RTN-CODE > WS-MAX-RETURN-CODE
MOVE RTN-CODE TO WS-MAX-RETURN-CODE
END-IF
IF RTN-OK
CALL 'STEP2' USING WS-DATA RTN-CODE-AREA
IF RTN-CODE > WS-MAX-RETURN-CODE
MOVE RTN-CODE TO WS-MAX-RETURN-CODE
END-IF
END-IF
IF RTN-OK
CALL 'STEP3' USING WS-DATA RTN-CODE-AREA
IF RTN-CODE > WS-MAX-RETURN-CODE
MOVE RTN-CODE TO WS-MAX-RETURN-CODE
END-IF
END-IF
* Set job-level return code
MOVE WS-MAX-RETURN-CODE TO RETURN-CODE.
💡 Practitioner's Insight: "The RETURN-CODE special register communicates the overall job status back to the operating system. JCL condition code checking (COND parameter or IF/THEN/ELSE) uses this value to control step execution. Setting RETURN-CODE at the end of your main program is critical for automated batch scheduling." — James Okafor
23.8 Error Structure Pattern
Return codes tell the caller that something went wrong and how severe it is. Error structures tell the caller what went wrong and why. Together, they provide comprehensive error communication.
GlobalBank's Standard Error Structure
*================================================================
* ERRSTRCP — GlobalBank Standard Error Structure
* Used by all subprograms for detailed error communication.
*================================================================
01 ERR-STRUCTURE.
05 ERR-PROGRAM-ID PIC X(8).
05 ERR-PARAGRAPH-ID PIC X(30).
05 ERR-TIMESTAMP PIC X(26).
05 ERR-SEVERITY PIC S9(4) COMP.
05 ERR-CATEGORY PIC X(4).
88 ERR-CAT-DATA VALUE 'DATA'.
88 ERR-CAT-FILE VALUE 'FILE'.
88 ERR-CAT-CALC VALUE 'CALC'.
88 ERR-CAT-AUTH VALUE 'AUTH'.
88 ERR-CAT-TECH VALUE 'TECH'.
05 ERR-CODE PIC X(8).
05 ERR-SHORT-MSG PIC X(40).
05 ERR-LONG-MSG PIC X(200).
05 ERR-FIELD-INFO.
10 ERR-FIELD-NAME PIC X(30).
10 ERR-FIELD-VAL PIC X(50).
10 ERR-EXPECTED PIC X(50).
05 ERR-CONTEXT.
10 ERR-ACCT-ID PIC X(10).
10 ERR-TXN-ID PIC X(15).
10 ERR-USER-ID PIC X(8).
Populating the Error Structure
PROCEDURE DIVISION USING LS-ACCOUNT-DATA
LS-RETURN-CODE
ERR-STRUCTURE.
VALIDATE-ACCOUNT.
MOVE 0 TO LS-RETURN-CODE
INITIALIZE ERR-STRUCTURE
* Validate account type
IF ACCT-TYPE NOT = 'CK' AND 'SV' AND 'MM'
MOVE 8 TO LS-RETURN-CODE
MOVE 'ACCTVAL' TO ERR-PROGRAM-ID
MOVE 'VALIDATE-ACCOUNT' TO ERR-PARAGRAPH-ID
MOVE FUNCTION CURRENT-DATE
TO ERR-TIMESTAMP
MOVE 8 TO ERR-SEVERITY
SET ERR-CAT-DATA TO TRUE
MOVE 'ACCT0001' TO ERR-CODE
MOVE 'Invalid account type'
TO ERR-SHORT-MSG
STRING 'Account type "' ACCT-TYPE
'" is not recognized. '
'Valid types are CK, SV, MM.'
DELIMITED BY SIZE
INTO ERR-LONG-MSG
MOVE 'ACCT-TYPE' TO ERR-FIELD-NAME
MOVE ACCT-TYPE TO ERR-FIELD-VAL
MOVE 'CK, SV, or MM' TO ERR-EXPECTED
MOVE ACCT-ID TO ERR-ACCT-ID
GOBACK
END-IF
...
The Caller's Error Handling
CALL 'ACCTVAL' USING WS-ACCOUNT-DATA
WS-RETURN-CODE
WS-ERR-STRUCTURE
IF WS-RETURN-CODE > 4
DISPLAY 'Error in ' ERR-PROGRAM-ID
' at ' ERR-PARAGRAPH-ID
DISPLAY 'Code: ' ERR-CODE
' - ' ERR-SHORT-MSG
IF ERR-FIELD-NAME NOT = SPACES
DISPLAY 'Field: ' ERR-FIELD-NAME
DISPLAY 'Value: ' ERR-FIELD-VAL
DISPLAY 'Expected: ' ERR-EXPECTED
END-IF
* Log to error file for batch reconciliation
CALL 'ERRLOG' USING WS-ERR-STRUCTURE
WS-LOG-RC
END-IF
23.9 Communication Areas — The COMMAREA Concept
In CICS online transaction processing (covered in detail in Chapters 30-31), programs communicate through a Communication Area (COMMAREA). While COMMAREA is a CICS concept, the pattern is useful in batch processing too.
A communication area is a single, comprehensive data structure that carries all information between programs:
*================================================================
* COMARECP — MedClaim Adjudication Communication Area
* Single structure carrying all data between adjudication
* engine components.
*================================================================
01 ADJUD-COMMAREA.
* Header — routing and control
05 CA-FUNCTION PIC X(4).
05 CA-STEP-NUMBER PIC 9(2).
05 CA-TIMESTAMP PIC X(26).
05 CA-USER-ID PIC X(8).
* Claim data — input
05 CA-CLAIM-DATA.
10 CA-CLAIM-ID PIC X(15).
10 CA-MEMBER-ID PIC X(12).
10 CA-PROVIDER-ID PIC X(10).
10 CA-DIAG-CODES.
15 CA-DIAG-CODE PIC X(7)
OCCURS 5 TIMES.
10 CA-PROC-CODES.
15 CA-PROC-CODE PIC X(5)
OCCURS 10 TIMES.
10 CA-AMOUNTS.
15 CA-CHARGE-AMT PIC S9(7)V99 COMP-3
OCCURS 10 TIMES.
10 CA-SERVICE-DATE PIC 9(8).
* Adjudication results — output
05 CA-RESULTS.
10 CA-ALLOWED-AMT PIC S9(7)V99 COMP-3.
10 CA-DEDUCTIBLE PIC S9(7)V99 COMP-3.
10 CA-COPAY PIC S9(7)V99 COMP-3.
10 CA-COINSURANCE PIC S9(7)V99 COMP-3.
10 CA-PAYMENT-AMT PIC S9(7)V99 COMP-3.
10 CA-DENY-REASON PIC X(4).
10 CA-ADJ-STATUS PIC X(2).
88 CA-APPROVED VALUE 'AP'.
88 CA-DENIED VALUE 'DN'.
88 CA-PENDING VALUE 'PN'.
88 CA-REVIEW VALUE 'RV'.
* Error communication
05 CA-ERROR-INFO.
10 CA-RETURN-CODE PIC S9(4) COMP.
88 CA-SUCCESS VALUE 0.
88 CA-WARNING VALUE 4.
88 CA-ERROR VALUE 8.
88 CA-SEVERE VALUE 12.
10 CA-ERROR-COUNT PIC 9(2).
10 CA-ERRORS.
15 CA-ERR-ENTRY OCCURS 10 TIMES.
20 CA-ERR-CODE PIC X(8).
20 CA-ERR-MSG PIC X(50).
20 CA-ERR-FIELD PIC X(20).
Advantages of the COMMAREA Pattern
- Single parameter: Only one data item is passed, eliminating parameter order and count mismatches.
- Self-documenting: The structure documents all inputs and outputs in one place.
- Extensible: New fields can be added at the end without breaking existing callers (as long as they pass at least as much storage as before).
- Portable to CICS: The same structure works for both batch CALL and CICS COMMAREA communication.
Disadvantages of the COMMAREA Pattern
- Monolithic: All programs see all fields, even those they do not use. A pricing module can accidentally modify eligibility fields.
- Storage overhead: Every caller allocates the full structure, even if it only uses a portion. A 3KB COMMAREA multiplied by thousands of concurrent CICS transactions adds up.
- Change management: Adding fields in the middle (rather than at the end) breaks backward compatibility because all field offsets shift.
- Testing complexity: To test one stage, you must initialize the entire COMMAREA, not just the fields that stage uses.
COMMAREA Sizing Guidelines
On z/OS CICS, COMMAREAs have practical size limits: - Maximum: 32,763 bytes (CICS limit) - Recommended: Under 4,000 bytes for performance - Typical: 1,000-3,000 bytes for complex applications
For batch processing via CALL, there is no system-imposed size limit, but very large COMMAREAs (over 10KB) should be reconsidered — they may indicate that the interface is carrying too much responsibility.
COMMAREA Versioning
When a COMMAREA needs to grow, the standard practice is to add new fields at the end and reduce a reserved FILLER area:
* Version 1.0
01 MY-COMMAREA.
05 CA-FIELD-A PIC X(10).
05 CA-FIELD-B PIC S9(9)V99 COMP-3.
05 CA-RESERVED PIC X(500).
* Total size: 517 bytes
* Version 2.0 — added CA-FIELD-C
01 MY-COMMAREA.
05 CA-FIELD-A PIC X(10).
05 CA-FIELD-B PIC S9(9)V99 COMP-3.
05 CA-FIELD-C PIC X(20).
05 CA-RESERVED PIC X(480).
* Total size: still 517 bytes
As long as the total size does not change, old callers compiled with version 1.0 continue to work because the overall COMMAREA length is unchanged. The old caller's CA-RESERVED area occupies the bytes where the new CA-FIELD-C lives, but since the old caller never references those bytes, there is no conflict.
⚠️ Critical Rule: Never add fields in the middle of a COMMAREA. Always add at the end (before CA-RESERVED). Inserting fields in the middle shifts all subsequent field offsets, breaking every program compiled with the old copybook.
⚖️ Design Trade-off: The choice between multiple focused parameters and a single COMMAREA depends on the system's architecture. For tightly coupled programs that always work together (like MedClaim's adjudication pipeline), the COMMAREA pattern reduces interface complexity. For loosely coupled utility programs (like date validation), separate parameters are cleaner.
23.10 The RETURN-CODE Special Register and Job-Level Communication
We briefly mentioned RETURN-CODE in Chapter 22. Let us now examine its relationship to parameter passing in more detail, because it represents a second communication channel between programs — one that operates outside the CALL USING mechanism.
RETURN-CODE in Subprograms
When a subprogram executes GOBACK, the current value of RETURN-CODE is propagated back to the caller. However, Language Environment (LE) may modify RETURN-CODE during the return process, making it unreliable as a primary communication mechanism. This is why we always use explicit parameters for return codes rather than relying on RETURN-CODE.
* UNRELIABLE pattern — do not use
CALL 'SUBPROG' USING WS-DATA
* Check RETURN-CODE after the call — may not reflect
* what SUBPROG set, depending on LE settings
* RELIABLE pattern — use this
CALL 'SUBPROG' USING WS-DATA WS-RETURN-CODE
* WS-RETURN-CODE is explicitly set by the subprogram
* via the LINKAGE SECTION and is always reliable
Setting the Job-Level Return Code
At the end of the main program, set RETURN-CODE to communicate the overall job status to JCL:
PROCEDURE DIVISION.
MAIN-LOGIC.
PERFORM PROCESS-ALL-RECORDS
* Propagate the highest severity to the job level
MOVE WS-MAX-RETURN-CODE TO RETURN-CODE
* Log the final status
EVALUATE TRUE
WHEN WS-MAX-RETURN-CODE = 0
DISPLAY 'Job completed successfully.'
WHEN WS-MAX-RETURN-CODE = 4
DISPLAY 'Job completed with warnings.'
WHEN WS-MAX-RETURN-CODE >= 8
DISPLAY 'Job completed with errors.'
END-EVALUATE
STOP RUN.
In the JCL, subsequent steps check this value:
//STEP1 EXEC PGM=MAINPROG
//STEP2 EXEC PGM=REPORTS,COND=(8,LT,STEP1)
//* STEP2 runs only if STEP1's RC is less than 8
//STEP3 EXEC PGM=CLEANUP,COND=(0,NE,STEP1)
//* STEP3 runs only if STEP1's RC is not equal to 0
Modern JCL uses IF/THEN/ELSE syntax which is clearer:
// IF (STEP1.RC < 8) THEN
//STEP2 EXEC PGM=REPORTS
// ELSE
//STEP2B EXEC PGM=ERRRPT
// ENDIF
💡 Practitioner's Insight: "At GlobalBank, our batch scheduler (Control-M) monitors RETURN-CODE from every job step. Return code 0 means success — the scheduler moves to the next job in the stream. Return code 4 means warning — the scheduler continues but flags the job for review during the morning operations meeting. Return code 8 or higher means error — the scheduler alerts the on-call team and holds dependent jobs. This entire automation chain depends on the COBOL program setting RETURN-CODE correctly." — Derek Washington
The Relationship Between Parameters and RETURN-CODE
Think of the communication hierarchy this way:
Parameter-level return codes (via CALL USING):
├── Used between CALL and GOBACK
├── Reliable and explicit
├── Support complex error structures
├── Support multiple simultaneous values
└── Recommended for all subprogram communication
Job-level return code (via RETURN-CODE special register):
├── Used between STOP RUN and JCL/scheduler
├── Single numeric value only
├── Communicates to the operating system
├── Controls job step execution
└── Set once at the end of the main program
23.11 GlobalBank Case Study: Standardized Error Handling
When Maria Chen modularized GlobalBank's account maintenance system (Chapter 22), she needed a way for all modules to communicate errors consistently. The challenge was that different developers had different error-handling styles:
- ACCTREAD returned 'Y' or 'N' in a flag field
- ACCTVAL used return codes 1, 2, 3, 4 (not the standard 0/4/8/12)
- ACCTCALC returned an error message but no return code
- ACCTUPD did not handle errors at all
Maria's standardization effort established three rules:
Rule 1: Every subprogram returns a standardized return code via the RETCODCP copybook.
Rule 2: Every subprogram that can fail populates the ERRSTRCP error structure.
Rule 3: The driver program evaluates the return code after every CALL and uses the error structure for logging and reporting.
The result was a consistent pattern across 23 programs:
* Standard call pattern used throughout GlobalBank
CALL 'module-name' USING function-area
RTN-CODE-AREA
ERR-STRUCTURE
PERFORM CHECK-RETURN-CODE
CHECK-RETURN-CODE.
EVALUATE TRUE
WHEN RTN-SUCCESS
CONTINUE
WHEN RTN-WARNING
ADD 1 TO WS-WARNING-COUNT
PERFORM LOG-WARNING
WHEN RTN-ERROR
ADD 1 TO WS-ERROR-COUNT
PERFORM LOG-ERROR
WHEN RTN-SEVERE
ADD 1 TO WS-SEVERE-COUNT
PERFORM LOG-ERROR
SET STOP-PROCESSING TO TRUE
WHEN RTN-FATAL
PERFORM LOG-ERROR
PERFORM EMERGENCY-SHUTDOWN
END-EVALUATE.
This standardization reduced debugging time by 40% because every developer knew exactly where to find error information and how to interpret it.
23.11 MedClaim Case Study: Adjudication Engine Parameters
MedClaim's claims adjudication engine is the heart of their system. A single claim passes through six processing stages, each implemented as a separate subprogram:
CLM-ADJUD (Driver)
├── CALL 'ADJELIG' — Eligibility verification
├── CALL 'ADJBNFT' — Benefit determination
├── CALL 'ADJPRIC' — Pricing/fee schedule lookup
├── CALL 'ADJDEDC' — Deductible and copay calculation
├── CALL 'ADJPAY' — Payment calculation
└── CALL 'ADJAUDT' — Adjudication audit trail
All six subprograms share the ADJUD-COMMAREA structure. Each stage reads the data it needs and writes its results into the appropriate section:
IDENTIFICATION DIVISION.
PROGRAM-ID. CLM-ADJUD.
*================================================================
* MedClaim Claims Adjudication Engine — Driver
* Orchestrates the six-stage adjudication pipeline.
*================================================================
DATA DIVISION.
WORKING-STORAGE SECTION.
COPY COMARECP.
PROCEDURE DIVISION.
ADJUDICATE-CLAIM.
* Stage 1: Eligibility
MOVE 'ELIG' TO CA-FUNCTION
MOVE 1 TO CA-STEP-NUMBER
CALL 'ADJELIG' USING ADJUD-COMMAREA
IF NOT CA-SUCCESS
PERFORM HANDLE-STAGE-ERROR
GOBACK
END-IF
* Stage 2: Benefits
MOVE 'BNFT' TO CA-FUNCTION
MOVE 2 TO CA-STEP-NUMBER
CALL 'ADJBNFT' USING ADJUD-COMMAREA
IF NOT CA-SUCCESS
PERFORM HANDLE-STAGE-ERROR
GOBACK
END-IF
* Stages 3-5: Pricing, Deductible, Payment
MOVE 'PRIC' TO CA-FUNCTION
MOVE 3 TO CA-STEP-NUMBER
CALL 'ADJPRIC' USING ADJUD-COMMAREA
IF NOT CA-SUCCESS
PERFORM HANDLE-STAGE-ERROR
GOBACK
END-IF
MOVE 'DEDC' TO CA-FUNCTION
MOVE 4 TO CA-STEP-NUMBER
CALL 'ADJDEDC' USING ADJUD-COMMAREA
IF NOT CA-SUCCESS
PERFORM HANDLE-STAGE-ERROR
GOBACK
END-IF
MOVE 'PAY ' TO CA-FUNCTION
MOVE 5 TO CA-STEP-NUMBER
CALL 'ADJPAY' USING ADJUD-COMMAREA
IF NOT CA-SUCCESS
PERFORM HANDLE-STAGE-ERROR
GOBACK
END-IF
* Stage 6: Audit (always runs, even if claim is denied)
MOVE 'AUDT' TO CA-FUNCTION
MOVE 6 TO CA-STEP-NUMBER
CALL 'ADJAUDT' USING ADJUD-COMMAREA
GOBACK.
HANDLE-STAGE-ERROR.
DISPLAY 'Adjudication error at stage '
CA-STEP-NUMBER
' for claim ' CA-CLAIM-ID
IF CA-ERROR-COUNT > 0
DISPLAY ' Error: ' CA-ERR-CODE(1)
' - ' CA-ERR-MSG(1)
END-IF.
Sarah Kim notes the advantages of this design: "When CMS changed the pricing rules for a specific procedure code, we modified only ADJPRIC. When a new deductible formula was introduced for high-deductible plans, we modified only ADJDEDC. Each change was contained to a single module, tested independently, and deployed without touching the rest of the pipeline."
23.13 Defensive Parameter Validation Patterns
The theme of Defensive Programming runs throughout this chapter. Here are the specific patterns that protect against parameter-related bugs.
Pattern 1: Validate All Inputs Before Processing
Never assume the caller passes valid data. Validate everything before your subprogram begins its core processing:
PROCEDURE DIVISION USING LS-ACCT-DATA
LS-AMOUNT
LS-RETURN-CODE
ERR-STRUCTURE.
VALIDATE-AND-PROCESS.
MOVE 0 TO LS-RETURN-CODE
INITIALIZE ERR-STRUCTURE
* Input validation gate — all checks before processing
PERFORM VALIDATE-ACCOUNT-DATA
IF LS-RETURN-CODE NOT = 0
GOBACK
END-IF
PERFORM VALIDATE-AMOUNT
IF LS-RETURN-CODE NOT = 0
GOBACK
END-IF
* Only reach here if all validations passed
PERFORM PROCESS-TRANSACTION
GOBACK.
This "validation gate" pattern ensures that the processing logic never sees invalid data.
Pattern 2: Guard Against Numeric Overflow
When performing calculations on parameters, always use ON SIZE ERROR:
COMPUTE LS-RESULT = LS-AMOUNT * LS-RATE
ON SIZE ERROR
MOVE 8 TO LS-RETURN-CODE
MOVE 'CALC0001' TO ERR-CODE
MOVE 'Arithmetic overflow in calculation'
TO ERR-SHORT-MSG
GOBACK
END-COMPUTE
Without ON SIZE ERROR, an overflow produces unpredictable results — truncation or data corruption — with no indication that anything went wrong. The caller processes incorrect data in good faith.
Pattern 3: Initialize All Output Parameters
Before any processing that might fail partway through, initialize all output parameters to known-safe values:
PROCESS-START.
* Initialize all outputs to safe defaults
MOVE 0 TO LS-RESULT-AMOUNT
MOVE SPACES TO LS-RESULT-NAME
MOVE SPACES TO LS-RESULT-MSG
MOVE 0 TO LS-RESULT-COUNT
MOVE 0 TO LS-RETURN-CODE
* Now process — if we GOBACK early due to an error,
* the output fields have safe default values rather
* than garbage from a previous call or uninitialized
* storage.
Pattern 4: The "Envelope" Pattern
Wrap your subprogram's core logic in an error-handling envelope that catches unexpected conditions:
PROCEDURE DIVISION USING LS-INPUT
LS-OUTPUT
LS-RETURN-CODE.
MAIN-ENTRY.
* Initialize
MOVE 0 TO LS-RETURN-CODE
INITIALIZE LS-OUTPUT
* Validate
PERFORM VALIDATE-INPUT
IF LS-RETURN-CODE NOT = 0
GOBACK
END-IF
* Process (the core logic)
PERFORM CORE-PROCESSING
* Final check
IF LS-RETURN-CODE = 0
PERFORM FINALIZE-OUTPUT
END-IF
GOBACK.
VALIDATE-INPUT.
* All input validation here
...
CORE-PROCESSING.
* All business logic here
...
FINALIZE-OUTPUT.
* Final formatting of output
...
This envelope — initialize, validate, process, finalize — provides a consistent structure that is easy to read, debug, and maintain.
Pattern 5: Document Parameter Contracts in Copybooks
The ultimate defensive measure is documentation. Every interface copybook should document:
*================================================================
* TXNIFCP — Transaction Interface Copybook
*
* PARAMETER CONTRACT:
*
* Input Fields (passed BY CONTENT recommended):
* TXN-ACCOUNT-ID Must be 10 alphanumeric characters
* TXN-AMOUNT Must be positive, max 999999.99
* TXN-TYPE Must be 'DEP', 'WDL', or 'XFR'
*
* Output Fields (set by subprogram):
* TXN-RESULT-CODE 0=OK, 4=warn, 8=err, 12=severe
* TXN-NEW-BALANCE Updated balance after transaction
* TXN-ERROR-MSG Descriptive message if error
*
* Preconditions:
* Account must exist and be in ACTIVE status
* For WDL: sufficient balance required
* For XFR: target account must be specified
*
* Side Effects:
* Updates ACCT-MASTER file (for TXN-TYPE = DEP, WDL)
* Writes to AUDIT-LOG sequential file
*================================================================
When both the caller and the called program adhere to this contract, parameter-related bugs are nearly eliminated.
23.14 Try It Yourself: Safe Parameter Passing
🧪 Try It Yourself: Build a string utility library that demonstrates all three parameter passing modes.
Step 1: Create a subprogram STRTRIM that trims trailing spaces from a string: - Parameter 1 (BY CONTENT): Input string PIC X(100) - Parameter 2 (BY REFERENCE): Output string PIC X(100) - Parameter 3 (BY REFERENCE): Actual length PIC S9(4) COMP - Parameter 4 (BY REFERENCE): Return code PIC S9(4) COMP
Step 2: Create a subprogram STRUPPER that converts a string to uppercase: - Parameter 1 (BY REFERENCE): String to convert (I/O) PIC X(100) - Parameter 2 (BY REFERENCE): Return code PIC S9(4) COMP
Step 3: Create a driver that: - Passes ' HELLO WORLD ' to STRTRIM using BY CONTENT to protect the input - Verifies that the original string is unchanged after the call - Passes the trimmed result to STRUPPER using BY REFERENCE - Verifies that the string was modified in place
Questions to answer: - What happens if you change STRTRIM's first parameter from BY CONTENT to BY REFERENCE? - Why is BY REFERENCE appropriate for STRUPPER but not for STRTRIM's input?
23.13 The RETURNING Clause in Detail
We mentioned the RETURNING clause briefly in the BY VALUE section. Let us explore it more fully, as it plays an important role in cross-language interoperability.
Syntax
CALL 'program-name' USING parameters
RETURNING identifier
The RETURNING clause captures a value from the called program's PROCEDURE DIVISION RETURNING clause:
* Called program
LINKAGE SECTION.
01 LS-INPUT PIC S9(9) COMP-5.
01 LS-RESULT PIC S9(9) COMP-5.
PROCEDURE DIVISION USING BY VALUE LS-INPUT
RETURNING LS-RESULT.
COMPUTE LS-RESULT = LS-INPUT * LS-INPUT
GOBACK.
RETURNING vs. Output Parameters
In pure COBOL-to-COBOL calls, RETURNING and output parameters (via CALL USING BY REFERENCE) achieve the same result. The choice is largely stylistic:
* Style 1: RETURNING (functional style)
CALL 'SQUARE' USING BY VALUE WS-INPUT
RETURNING WS-RESULT
* Style 2: Output parameter (traditional COBOL style)
CALL 'SQUARE' USING WS-INPUT WS-RESULT
However, when calling C functions, RETURNING is essential because C functions return values through the function return mechanism, not through output parameters:
// C function
int square(int x) {
return x * x;
}
* COBOL calling the C function
CALL 'square' USING BY VALUE WS-X
RETURNING WS-RESULT
Restrictions on RETURNING
- The RETURNING data item must be elementary (not a group item).
- It must be compatible with the called program's RETURNING declaration.
- RETURNING is commonly used with COMP-5, COMP-1, COMP-2, and POINTER types.
- Some compilers do not support RETURNING for COBOL-to-COBOL calls — check your compiler documentation.
23.14 Designing Parameter Blocks — Practical Patterns
Beyond individual parameters, real-world subprograms often need structured parameter blocks. Here are the patterns you will encounter and should know how to design.
Pattern 1: The Request-Response Block
This pattern bundles the request (input) and response (output) into a single structure:
*================================================================
* ACCTLKCP — Account Lookup Request/Response
*================================================================
01 ACCT-LOOKUP-BLOCK.
* Request section (input)
05 ALK-REQUEST.
10 ALK-FUNCTION PIC X(4).
88 ALK-BY-ID VALUE 'BYID'.
88 ALK-BY-NAME VALUE 'BYNM'.
88 ALK-BY-SSN VALUE 'BSSN'.
10 ALK-SEARCH-KEY PIC X(30).
10 ALK-MAX-RESULTS PIC 9(3) VALUE 10.
* Response section (output)
05 ALK-RESPONSE.
10 ALK-RESULT-COUNT PIC 9(3).
10 ALK-RESULTS.
15 ALK-RESULT-ENTRY OCCURS 10 TIMES.
20 ALK-RES-ACCT-ID PIC X(10).
20 ALK-RES-NAME PIC X(30).
20 ALK-RES-TYPE PIC X(2).
20 ALK-RES-STATUS PIC X(1).
* Status section
05 ALK-STATUS.
10 ALK-RETURN-CODE PIC S9(4) COMP.
88 ALK-SUCCESS VALUE 0.
88 ALK-NOT-FOUND VALUE 4.
88 ALK-ERROR VALUE 8.
10 ALK-ERROR-MSG PIC X(50).
The caller populates the request section, calls the subprogram, and reads the response section:
INITIALIZE ACCT-LOOKUP-BLOCK
SET ALK-BY-NAME TO TRUE
MOVE 'CHEN' TO ALK-SEARCH-KEY
MOVE 10 TO ALK-MAX-RESULTS
CALL 'ACCTLKUP' USING ACCT-LOOKUP-BLOCK
IF ALK-SUCCESS
DISPLAY 'Found ' ALK-RESULT-COUNT ' accounts'
PERFORM VARYING WS-IDX FROM 1 BY 1
UNTIL WS-IDX > ALK-RESULT-COUNT
DISPLAY ALK-RES-ACCT-ID(WS-IDX) ' '
ALK-RES-NAME(WS-IDX)
END-PERFORM
END-IF
Pattern 2: The Configuration Block
For subprograms with many optional settings, a configuration block consolidates them:
*================================================================
* RPTCFGCP — Report Configuration Block
*================================================================
01 RPT-CONFIG.
05 RPT-TITLE PIC X(60).
05 RPT-SUBTITLE PIC X(60).
05 RPT-DATE-FORMAT PIC X(10).
88 RPT-DATE-MDY VALUE 'MM/DD/YYYY'.
88 RPT-DATE-DMY VALUE 'DD/MM/YYYY'.
88 RPT-DATE-YMD VALUE 'YYYY-MM-DD'.
88 RPT-DATE-ISO VALUE 'ISO-8601'.
05 RPT-LINES-PER-PAGE PIC 9(3) VALUE 55.
05 RPT-COLUMNS PIC 9(3) VALUE 132.
05 RPT-ORIENTATION PIC X(1) VALUE 'P'.
88 RPT-PORTRAIT VALUE 'P'.
88 RPT-LANDSCAPE VALUE 'L'.
05 RPT-PAGE-NUMBERS PIC X(1) VALUE 'Y'.
88 RPT-SHOW-PAGES VALUE 'Y'.
88 RPT-HIDE-PAGES VALUE 'N'.
05 RPT-TOTALS-FLAG PIC X(1) VALUE 'Y'.
88 RPT-SHOW-TOTALS VALUE 'Y'.
88 RPT-HIDE-TOTALS VALUE 'N'.
Pattern 3: The Multi-Error Block
When a subprogram can detect multiple errors in a single call (as in MedClaim's validation):
*================================================================
* MULTIERR — Multi-Error Communication Block
*================================================================
01 MULTI-ERROR-BLOCK.
05 MEB-MAX-ERRORS PIC 9(2) VALUE 10.
05 MEB-ERROR-COUNT PIC 9(2) VALUE 0.
05 MEB-HIGHEST-SEV PIC S9(4) COMP VALUE 0.
05 MEB-ERRORS.
10 MEB-ERROR-ENTRY OCCURS 10 TIMES.
15 MEB-ERR-SEV PIC S9(4) COMP.
15 MEB-ERR-CODE PIC X(8).
15 MEB-ERR-FIELD PIC X(30).
15 MEB-ERR-VALUE PIC X(30).
15 MEB-ERR-MSG PIC X(80).
The caller initializes the block, passes it to the subprogram, and iterates through the errors:
INITIALIZE MULTI-ERROR-BLOCK
MOVE 10 TO MEB-MAX-ERRORS
CALL 'VALIDATE' USING WS-DATA-RECORD
MULTI-ERROR-BLOCK
IF MEB-ERROR-COUNT > 0
DISPLAY MEB-ERROR-COUNT ' errors found:'
PERFORM VARYING WS-IDX FROM 1 BY 1
UNTIL WS-IDX > MEB-ERROR-COUNT
DISPLAY ' ' MEB-ERR-FIELD(WS-IDX)
': ' MEB-ERR-MSG(WS-IDX)
END-PERFORM
END-IF
23.15 Advanced Topic: Passing Tables and Complex Structures
When passing tables (arrays) between programs, the entire table is passed as a contiguous block of storage:
* Calling program
01 WS-RATE-TABLE.
05 WS-RATE-ENTRY OCCURS 100 TIMES.
10 WS-RATE-CODE PIC X(4).
10 WS-RATE-VALUE PIC S9(5)V9(4) COMP-3.
10 WS-RATE-EFF-DT PIC 9(8).
CALL 'RATELKUP' USING WS-RATE-TABLE
WS-LOOKUP-CODE
WS-RESULT-RATE
WS-RETURN-CODE
* Called program
LINKAGE SECTION.
01 LS-RATE-TABLE.
05 LS-RATE-ENTRY OCCURS 100 TIMES.
10 LS-RATE-CODE PIC X(4).
10 LS-RATE-VALUE PIC S9(5)V9(4) COMP-3.
10 LS-RATE-EFF-DT PIC 9(8).
⚠️ Critical: The OCCURS count in the LINKAGE SECTION must match (or be less than or equal to) the caller's OCCURS count. If the called program declares OCCURS 200 but the caller only allocated space for 100 entries, accessing entries 101-200 causes a storage overlay.
Passing Variable-Occurrence Tables
For tables with a variable number of entries, pass the actual count as a separate parameter:
* Calling program
01 WS-ENTRY-COUNT PIC S9(4) COMP VALUE 47.
01 WS-ENTRY-TABLE.
05 WS-ENTRY OCCURS 100 TIMES.
10 WS-ENT-CODE PIC X(8).
10 WS-ENT-VALUE PIC S9(9)V99 COMP-3.
CALL 'PROCESS' USING BY CONTENT WS-ENTRY-COUNT
BY REFERENCE WS-ENTRY-TABLE
BY REFERENCE WS-RETURN-CODE
The called program uses WS-ENTRY-COUNT to limit its processing:
LINKAGE SECTION.
01 LS-ENTRY-COUNT PIC S9(4) COMP.
01 LS-ENTRY-TABLE.
05 LS-ENTRY OCCURS 100 TIMES.
10 LS-ENT-CODE PIC X(8).
10 LS-ENT-VALUE PIC S9(9)V99 COMP-3.
01 LS-RETURN-CODE PIC S9(4) COMP.
PROCEDURE DIVISION USING LS-ENTRY-COUNT
LS-ENTRY-TABLE
LS-RETURN-CODE.
PROCESS-TABLE.
IF LS-ENTRY-COUNT < 1 OR LS-ENTRY-COUNT > 100
MOVE 8 TO LS-RETURN-CODE
GOBACK
END-IF
PERFORM VARYING WS-IDX FROM 1 BY 1
UNTIL WS-IDX > LS-ENTRY-COUNT
PERFORM PROCESS-ONE-ENTRY
END-PERFORM
GOBACK.
23.16 GnuCOBOL Parameter Passing Notes
If you are working with GnuCOBOL in the Student Mainframe Lab, here are important differences in parameter passing behavior:
BY VALUE Support
GnuCOBOL supports BY VALUE for most elementary types, including: - Binary integers (COMP, COMP-5, BINARY) - Floating-point (COMP-1, COMP-2) - Single-byte alphanumeric (PIC X) - POINTER
* GnuCOBOL BY VALUE example
CALL 'c_function' USING BY VALUE WS-INTEGER
BY REFERENCE WS-BUFFER
C Interop Compilation
For C interoperability, compile and link as follows:
# Compile C function
gcc -c -o myfunction.o myfunction.c
# Compile COBOL program and link with C object
cobc -x -o myprog myprog.cbl myfunction.o
Data Type Mapping — COBOL to C
| COBOL Type | C Type | Notes |
|---|---|---|
| PIC S9(9) COMP-5 | int (32-bit) | Most common for BY VALUE |
| PIC S9(18) COMP-5 | long long (64-bit) | Large integers |
| COMP-1 | float | Single precision |
| COMP-2 | double | Double precision |
| PIC X(n) | char[n] | BY REFERENCE (pointer) |
| USAGE POINTER | void* | Address values |
OMITTED Support
GnuCOBOL supports OMITTED parameters. The called program checks for NULL pointers:
IF ADDRESS OF LS-OPTIONAL-PARAM = NULL
PERFORM USE-DEFAULTS
END-IF
The RETURNING Clause
GnuCOBOL fully supports RETURNING for both COBOL-to-COBOL and COBOL-to-C calls. This is the standard way to capture C function return values.
23.17 Common Pitfalls with Parameter Passing
Pitfall 1: Assuming BY REFERENCE Is BY CONTENT
* DANGEROUS: Caller assumes subprogram won't modify input
CALL 'VALIDATOR' USING WS-IMPORTANT-DATA
WS-RETURN-CODE
* If VALIDATOR modifies LS-DATA (parameter 1), the
* caller's WS-IMPORTANT-DATA is modified too!
* SAFE: Use BY CONTENT if data must be preserved
CALL 'VALIDATOR' USING BY CONTENT WS-IMPORTANT-DATA
BY REFERENCE WS-RETURN-CODE
Pitfall 2: BY VALUE Size Mismatches
* WRONG: PIC 9(4) COMP is 2 bytes, but C expects 4-byte int
01 WS-COUNT PIC 9(4) COMP.
CALL 'c_func' USING BY VALUE WS-COUNT
* CORRECT: Use COMP-5 or ensure 4-byte binary
01 WS-COUNT PIC S9(9) COMP-5.
CALL 'c_func' USING BY VALUE WS-COUNT
Pitfall 3: Modifying BY CONTENT Parameters in the Called Program
* The called program CAN modify a BY CONTENT parameter —
* it just modifies the copy, not the original.
* But this is confusing to future maintainers!
PROCEDURE DIVISION USING LS-INPUT-DATA
LS-RETURN-CODE.
MOVE 'MODIFIED' TO LS-INPUT-DATA
* This "works" but is misleading — the caller
* passed BY CONTENT to protect its data.
* Future readers will wonder why data is being
* modified that the caller didn't want modified.
✅ Best Practice: Even though modifying a BY CONTENT parameter is technically harmless (only the copy changes), avoid doing so. It confuses maintainers who see the modification and don't realize the caller used BY CONTENT. Treat BY CONTENT parameters as read-only.
23.18 Try It Yourself: Building an Error-Reporting Validation Module
🧪 Try It Yourself: Build a comprehensive validation subprogram that uses all the patterns from this chapter.
Scenario: Create a customer data validation subprogram (CUSTVAL) that validates a customer record with multiple fields.
Step 1: Define the customer record copybook (CUSTRECP.cpy):
01 CUST-RECORD.
05 CUST-ID PIC X(8).
05 CUST-NAME PIC X(30).
05 CUST-DOB PIC 9(8).
05 CUST-PHONE PIC X(10).
05 CUST-EMAIL PIC X(50).
05 CUST-STATUS PIC X(1).
Step 2: Define the multi-error block (use MULTIERR pattern from Section 23.14).
Step 3: Write CUSTVAL that: - Receives the customer record BY CONTENT (protect input) - Receives the multi-error block BY REFERENCE (output) - Validates each field: - CUST-ID: not blank, must be alphanumeric - CUST-NAME: not blank - CUST-DOB: valid date in YYYYMMDD format (call DTEVALID if available, or validate inline) - CUST-PHONE: must be 10 digits if not blank - CUST-EMAIL: must contain '@' if not blank - CUST-STATUS: must be 'A', 'I', or 'C' - Accumulates all errors (not just the first one) - Sets the highest severity as the overall return code
Step 4: Write a driver program that tests with: - A valid customer record (expect RC=0) - A record with blank ID and invalid status (expect RC=8, 2 errors) - A record with invalid date and phone (expect RC=8, 2 errors)
Questions to answer: - Why is BY CONTENT used for the customer record? - Why is BY REFERENCE used for the error block? - What happens if you pass the customer record BY REFERENCE and the subprogram accidentally modifies a field?
23.19 Chapter Summary
This chapter has explored the nuances of parameter passing in COBOL subprograms:
- BY REFERENCE (the default) passes the address of the caller's data. The called program shares the caller's storage and can modify it. Use for output parameters and bidirectional communication.
- BY CONTENT passes the address of a temporary copy. The caller's data is protected from modification. Use for input parameters that must not be changed, and for passing literals.
- BY VALUE passes the actual value, not an address. Use for interoperability with C and other languages that expect value parameters.
- OMITTED allows optional parameters. The called program must check for null addresses before accessing omitted parameters.
- Length fields communicate the actual size of variable-length data, enabling safe processing.
- Return codes (0/4/8/12/16) provide standardized severity communication. Use copybooks with level 88 condition names for clean evaluation.
- Error structures provide detailed "what went wrong" information beyond the simple severity of a return code.
- Communication areas (COMMAREA) bundle all interface data into a single structure, simplifying parameter management for tightly coupled program chains.
Additional concepts explored in this chapter:
- The RETURNING clause captures function return values, essential for calling C functions from COBOL.
- The request-response block pattern bundles input and output into a single structure with clear sections.
- The configuration block pattern consolidates optional settings into a structured parameter.
- The multi-error block pattern allows subprograms to report all errors in a single call, not just the first one.
- Defensive parameter validation — the validation gate, overflow guards, output initialization, envelope pattern, and documented contracts — prevents the most common parameter-related bugs.
- RETURN-CODE operates outside the parameter passing mechanism and communicates the job-level condition code to the operating system and JCL scheduler.
The parameter passing patterns in this chapter form the contract language of modular COBOL. When every program in your system follows the same conventions — the same return code values, the same error structure, the same copybook-based interfaces — the system becomes dramatically easier to build, test, debug, and maintain. As the Defensive Programming theme emphasizes, the effort invested in designing safe parameter interfaces pays dividends every time a production incident is avoided because a subprogram caught invalid input before it could cause damage.
"I've seen shops where every developer invented their own return code scheme. One used 0 and 1. Another used 'Y' and 'N'. A third used the SQLCODE as the return code. Debugging was archaeological work — you had to excavate each program's conventions before you could understand what it was telling you. Standardize. Please, standardize." — Priya Kapoor
In the next chapter, we will explore nested programs — a different way to organize modular COBOL code where subprograms are defined within the same source file as their caller, sharing a single compilation unit and offering unique data-sharing mechanisms through the GLOBAL clause.