25 min read

> "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

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:

  1. The called program needs to return data through the parameter — output parameters, updated records, computed results.
  2. Bidirectional communication — the parameter carries input data to the subprogram and the subprogram modifies it to carry output data back.
  3. 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:

  1. You want to protect input data — the called program cannot accidentally modify data the caller depends on.
  2. You're passing literals or expressions — BY CONTENT allows you to pass literal values and arithmetic expressions.
  3. 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:

  1. 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)

  2. No group items: You cannot pass a group item BY VALUE.

  3. Size limits: The item must fit in a register (typically 1, 2, 4, or 8 bytes).

  4. 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

  1. Single parameter: Only one data item is passed, eliminating parameter order and count mismatches.
  2. Self-documenting: The structure documents all inputs and outputs in one place.
  3. 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).
  4. Portable to CICS: The same structure works for both batch CALL and CICS COMMAREA communication.

Disadvantages of the COMMAREA Pattern

  1. Monolithic: All programs see all fields, even those they do not use. A pricing module can accidentally modify eligibility fields.
  2. 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.
  3. Change management: Adding fields in the middle (rather than at the end) breaks backward compatibility because all field offsets shift.
  4. 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

  1. The RETURNING data item must be elementary (not a group item).
  2. It must be compatible with the called program's RETURNING declaration.
  3. RETURNING is commonly used with COMP-5, COMP-1, COMP-2, and POINTER types.
  4. 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.