23 min read

In the early decades of COBOL programming, it was common to find programs spanning tens of thousands of lines -- monolithic behemoths that handled input validation, business logic, file processing, report generation, and error handling all within a...

Chapter 17: Subprograms and the CALL Statement

Introduction: Why Modular Programming Matters in COBOL

In the early decades of COBOL programming, it was common to find programs spanning tens of thousands of lines -- monolithic behemoths that handled input validation, business logic, file processing, report generation, and error handling all within a single source file. These programs were difficult to understand, painful to modify, and nearly impossible to test in isolation. When a change was needed in a date validation routine used in twenty different places, a programmer had to locate and modify every occurrence, hoping nothing was missed.

Modular programming solves these problems by decomposing large programs into smaller, self-contained units called subprograms (also known as subroutines or called programs). Each subprogram performs a specific, well-defined task and communicates with its caller through a formal parameter interface. This approach delivers several critical benefits:

  • Reusability: A date validation subprogram written once can be called from dozens of different main programs without duplicating a single line of code.
  • Maintainability: When the validation rules change, you modify one subprogram. Every caller automatically gets the updated behavior.
  • Testability: Each subprogram can be tested independently with a simple test driver, long before the full system is assembled.
  • Division of labor: Different programmers can work on different subprograms simultaneously, each with a clear interface contract.
  • Encapsulation: The internal logic of a subprogram is hidden from callers. As long as the interface remains stable, the implementation can be completely rewritten.

COBOL has supported external subprogram calls since its earliest standards, and this support has grown richer with each revision. The COBOL-85 standard introduced nested programs and the COMMON clause. COBOL 2002 added the RETURNING clause and recursive programs. IBM Enterprise COBOL, Micro Focus COBOL, and GnuCOBOL all provide robust implementations of these features.

This chapter covers everything you need to write, call, and manage COBOL subprograms: from the basic CALL statement through advanced topics like parameter passing modes, the CANCEL statement, nested programs, and inter-language calls.


17.1 The CALL Statement: Calling External Subprograms

The CALL statement transfers control from one COBOL program (the caller) to another (the called program or subprogram). When the subprogram finishes its work, it returns control to the statement following the CALL.

Basic Syntax

CALL 'SUBPROG' USING parameter-1 parameter-2 ...

Or with a variable:

CALL WS-PROGRAM-NAME USING parameter-1 parameter-2 ...

The CALL statement has several optional phrases:

CALL {literal | identifier}
    [USING {[BY REFERENCE] | BY CONTENT | BY VALUE} parameter ...]
    [RETURNING identifier]
    [ON EXCEPTION imperative-statement]
    [NOT ON EXCEPTION imperative-statement]
    [ON OVERFLOW imperative-statement]
[END-CALL]

The ON EXCEPTION phrase (or the older ON OVERFLOW) executes when the called program cannot be found or loaded. This is particularly important for dynamic calls where the program might not exist in the load library:

       CALL WS-PROG-NAME USING WS-PARAMETERS
           ON EXCEPTION
               DISPLAY 'ERROR: Program ' WS-PROG-NAME
                   ' not found'
               MOVE 99 TO WS-RETURN-CODE
           NOT ON EXCEPTION
               MOVE RETURN-CODE TO WS-RETURN-CODE
       END-CALL

17.2 Static vs. Dynamic CALL

COBOL supports two fundamentally different ways of calling subprograms, and the distinction has significant implications for performance, flexibility, and deployment.

Static CALL (CALL Literal with NODYNAM)

A static call uses a literal (a string enclosed in quotes) as the program name and is resolved at link-edit time (also called bind time). The compiler generates a direct reference to the subprogram, and the linker combines the main program and subprogram into a single load module.

      * Static call -- resolved at link time
       CALL 'PAYCALC' USING WS-EMPLOYEE-DATA
                             WS-RESULT-DATA
                             WS-STATUS

Characteristics of static calls:

Aspect Static Call Behavior
Resolution At link-edit time
Load module size Larger (includes all subprograms)
Runtime performance Faster (no load overhead)
Memory usage All subprograms in memory at once
Flexibility Less flexible; changes require re-link
CANCEL Not applicable (cannot cancel)

To compile for static calling on IBM Enterprise COBOL, use the NODYNAM compiler option:

PARM='NODYNAM,LIB,OBJECT'

See Example 1: Static Call for a complete working program.

Dynamic CALL (CALL Identifier or CALL Literal with DYNAM)

A dynamic call resolves the subprogram name at runtime. The Language Environment runtime locates the subprogram in the load library (STEPLIB, JOBLIB, or link pack area), loads it into memory, and transfers control.

There are two ways to make a dynamic call:

Method 1: Using an identifier (variable)

       01  WS-PROG-NAME  PIC X(08) VALUE 'PAYCALC'.
       ...
       CALL WS-PROG-NAME USING WS-DATA

When you CALL a variable instead of a literal, the call is always dynamic regardless of compiler options.

Method 2: Using a literal with the DYNAM compiler option

      * With DYNAM option, even literal calls become dynamic
       CALL 'PAYCALC' USING WS-DATA

When compiled with the DYNAM option, CALL 'literal' statements are treated as dynamic calls.

Characteristics of dynamic calls:

Aspect Dynamic Call Behavior
Resolution At runtime
Load module size Smaller (only main program)
Runtime performance Slightly slower (load overhead on first call)
Memory usage Subprograms loaded on demand
Flexibility Very flexible; can swap subprogram without re-linking main
CANCEL Supported (release from memory)

See Example 2: Dynamic Call for a program that dynamically selects which subprogram to call based on transaction type.

Compiler Options Controlling Static/Dynamic (IBM Enterprise COBOL)

Option Effect
NODYNAM CALL 'literal' is static (default on many systems)
DYNAM CALL 'literal' is treated as dynamic
N/A CALL identifier is always dynamic regardless of options

Important: The DYNAM/NODYNAM option only affects CALL 'literal' statements. A CALL identifier is always dynamic. Many shops standardize on NODYNAM and use identifiers when they specifically want dynamic behavior.

When to Use Each

Use static calls when: - The subprogram is always needed and will always be present - Maximum runtime performance is critical - You want the simplest compile-link-go process - The subprogram is small and the load module size increase is acceptable

Use dynamic calls when: - The subprogram may or may not be needed at runtime - You want to replace subprograms without re-linking the main program - Memory conservation is important (load on demand) - You need to CANCEL and reinitialize subprograms - The system uses a plugin or strategy pattern where different subprograms are selected at runtime


17.3 The LINKAGE SECTION: Defining Parameters

The LINKAGE SECTION in a subprogram defines the data items that the subprogram receives from its caller. These items do not allocate memory in the subprogram; instead, they describe the layout of memory that belongs to the calling program.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-LOCAL-DATA    PIC X(10).

       LINKAGE SECTION.
       01  LS-INPUT-RECORD.
           05  LS-FIELD-A   PIC X(10).
           05  LS-FIELD-B   PIC 9(05).
       01  LS-OUTPUT-AREA.
           05  LS-RESULT    PIC 9(07)V99.
           05  LS-STATUS    PIC 9(02).

Key points about the LINKAGE SECTION:

  1. No memory allocation: LINKAGE SECTION items describe data that resides in the caller's memory. The subprogram accesses the caller's data directly (for BY REFERENCE parameters).

  2. Naming convention: Many shops prefix LINKAGE SECTION items with LS- to distinguish them from WORKING-STORAGE items (WS-). This is a convention, not a requirement.

  3. Data description must match: The PICTURE clauses and structure in the LINKAGE SECTION must be compatible with what the caller passes. If the caller passes a PIC X(20) field and the LINKAGE SECTION describes it as PIC 9(20), the data will be reinterpreted, potentially causing errors.

  4. Group items are common: It is common practice to pass group items (records) rather than many individual elementary items. This reduces the number of parameters and makes the interface easier to manage.


17.4 PROCEDURE DIVISION USING: Declaring the Parameter List

The subprogram's PROCEDURE DIVISION must include a USING clause that names the LINKAGE SECTION items it expects to receive:

       PROCEDURE DIVISION USING LS-INPUT-RECORD
                                LS-OUTPUT-AREA.

Rules for PROCEDURE DIVISION USING:

  • Each item named in USING must be defined in the LINKAGE SECTION (01 or 77 level).
  • The order of parameters must match the order in the caller's CALL ... USING.
  • The number of parameters must match between caller and callee.
  • If any parameter is BY VALUE, it must be specified in the PROCEDURE DIVISION USING as well.

Matching Caller and Callee

The parameter lists are matched positionally, not by name. The first parameter in CALL ... USING corresponds to the first parameter in PROCEDURE DIVISION USING, and so on:

      * In the calling program:
       CALL 'MYSUB' USING WS-CUSTOMER-REC
                          WS-RESULT-AREA
                          WS-ERROR-CODE

      * In the called program:
       PROCEDURE DIVISION USING LS-INPUT-DATA
                                LS-OUTPUT-DATA
                                LS-STATUS.
      * WS-CUSTOMER-REC  <-->  LS-INPUT-DATA   (1st parameter)
      * WS-RESULT-AREA   <-->  LS-OUTPUT-DATA   (2nd parameter)
      * WS-ERROR-CODE    <-->  LS-STATUS         (3rd parameter)

The names do not need to match. What matters is that the size and structure of each parameter pair are compatible.


17.5 Parameter Passing Modes

COBOL provides three parameter passing modes, each with different semantics for how the caller's data is shared with the called program.

BY REFERENCE (Default)

When a parameter is passed BY REFERENCE, the subprogram receives the memory address of the caller's data. The caller and callee share the same physical memory location. Any modification the subprogram makes to the parameter is immediately visible to the caller.

       CALL 'MYSUB' USING BY REFERENCE WS-COUNTER

Since BY REFERENCE is the default, you can omit it:

       CALL 'MYSUB' USING WS-COUNTER

When to use BY REFERENCE: - When the subprogram needs to return data to the caller through the parameter - When you want maximum efficiency (no data copying) - For large data structures where copying would be expensive

BY CONTENT

When a parameter is passed BY CONTENT, the compiler creates a temporary copy of the caller's data and passes the address of that copy to the subprogram. The subprogram can read and even modify the copy, but the caller's original data remains unchanged.

       CALL 'MYSUB' USING BY CONTENT WS-ORIGINAL-VALUE

When to use BY CONTENT: - When you want to protect the caller's data from modification - When passing literals or expressions that cannot be modified - When you need read-only semantics for safety

Important: BY CONTENT does not mean the parameter is read-only in the subprogram. The subprogram can modify the temporary copy. The protection is on the caller's side -- the original variable will not be changed.

BY VALUE

When a parameter is passed BY VALUE, the actual value is placed directly on the call stack (not an address). This is the calling convention used by C and other languages.

       CALL 'MYSUB' USING BY VALUE WS-INTEGER-FIELD

In the called program's PROCEDURE DIVISION, BY VALUE must be explicitly stated:

       PROCEDURE DIVISION USING BY VALUE LS-INTEGER-FIELD.

Restrictions on BY VALUE: - The data item should be a binary integer type (COMP, COMP-5, BINARY) - Group items cannot be passed BY VALUE - BY VALUE is primarily used when calling C functions or other non-COBOL programs - Not all COBOL compilers support BY VALUE for COBOL-to-COBOL calls

See Example 3: Parameter Passing Modes for a working demonstration of all three modes.

Mixing Passing Modes

You can mix passing modes in a single CALL statement. The passing mode applies to all subsequent parameters until a different mode is specified:

       CALL 'MYSUB' USING BY REFERENCE WS-OUTPUT-AREA
                          BY CONTENT   WS-INPUT-DATA
                                       WS-CONTROL-FLAGS
                          BY REFERENCE WS-STATUS

In this example, WS-OUTPUT-AREA and WS-STATUS are BY REFERENCE, while WS-INPUT-DATA and WS-CONTROL-FLAGS are BY CONTENT (the BY CONTENT carries forward until changed).

Summary Table

Mode Caller's Data Modified? Mechanism Primary Use
BY REFERENCE Yes Passes address of original Output parameters, shared data
BY CONTENT No Passes address of a copy Input-only parameters, protection
BY VALUE No Passes value on stack C interoperability

17.6 The LENGTH OF Special Register

When passing data to a subprogram, you sometimes need to communicate the length of a variable-length or variable-format data area. The LENGTH OF special register provides the number of bytes occupied by a data item:

       01  WS-BUFFER     PIC X(500).
       01  WS-BUF-LENGTH PIC 9(04) COMP.
       ...
       MOVE LENGTH OF WS-BUFFER TO WS-BUF-LENGTH
       CALL 'PROCESS' USING WS-BUFFER
                             WS-BUF-LENGTH

You can also pass LENGTH OF directly:

       CALL 'PROCESS' USING WS-BUFFER
                             BY CONTENT LENGTH OF WS-BUFFER

This pattern is especially useful when: - The subprogram handles buffers of varying sizes - You are interfacing with system services that require explicit length parameters - The subprogram needs to validate that received data fits within expected bounds


17.7 RETURN-CODE Special Register

The RETURN-CODE special register is a numeric field that serves as a communication channel between a calling program and a called program. When a subprogram sets RETURN-CODE before returning with GOBACK, the calling program can examine RETURN-CODE after the CALL to determine the outcome.

In the called program:

       IF validation-failed
           MOVE 8 TO RETURN-CODE
       ELSE
           MOVE 0 TO RETURN-CODE
       END-IF
       GOBACK

In the calling program:

       CALL 'VALIDATE' USING WS-DATA
       IF RETURN-CODE > 0
           DISPLAY 'Validation failed with RC=' RETURN-CODE
       END-IF

Conventional Return Code Values

Many mainframe shops follow the IBM convention for return code values:

Value Meaning
0 Success; no errors
4 Warning; processing completed with minor issues
8 Error; processing may be incomplete
12 Severe error; significant problems
16 Critical error; processing could not continue

Following this convention makes programs easier to understand and maintain because the meaning of return codes is consistent across the entire system.


17.8 The RETURNING Clause (COBOL 2002+)

The RETURNING clause, introduced in COBOL 2002, provides a more explicit and structured way to return a value from a subprogram:

In the called program:

       LINKAGE SECTION.
       01  LS-INPUT       PIC X(20).
       01  LS-RETURN-VAL  PIC S9(04) COMP.

       PROCEDURE DIVISION USING LS-INPUT
                          RETURNING LS-RETURN-VAL.

In the calling program:

       CALL 'VALIDATE' USING WS-INPUT-DATA
                        RETURNING WS-RESULT

The RETURNING clause is cleaner than using RETURN-CODE because: - The return value is explicitly part of the interface definition - It can be any data type, not just a numeric code - It makes the subprogram's contract more visible and self-documenting

See Example 4: Return Code for a demonstration of both RETURN-CODE and RETURNING.

Note: When RETURNING is used, the value is also placed in the RETURN-CODE special register for backward compatibility (on most compilers).


17.9 GOBACK vs. STOP RUN in Subprograms

When a subprogram finishes its work, it must return control to the calling program. There are two statements that can end a program's execution, but they have very different effects:

GOBACK

GOBACK returns control to the statement following the CALL in the calling program. The run unit continues. GOBACK is the correct way to exit a subprogram:

       PROCEDURE DIVISION USING LS-PARAMETERS.
       0000-PROCESS.
           PERFORM actual-work
           GOBACK.

GOBACK can also be used in a main program, where it behaves like STOP RUN.

STOP RUN

STOP RUN terminates the entire run unit -- the main program and all subprograms. If a subprogram executes STOP RUN, the calling program does not get control back. The entire application ends.

Rule: Never use STOP RUN in a subprogram. Always use GOBACK.

The only place STOP RUN belongs is in the main program's exit logic:

      * In the main program -- STOP RUN is appropriate here
       0000-MAIN.
           PERFORM process-all-records
           STOP RUN.

EXIT PROGRAM

EXIT PROGRAM is an older alternative to GOBACK in a subprogram. In a called program, EXIT PROGRAM returns control to the caller. In a main program, EXIT PROGRAM is ignored and execution continues to the next statement. Most modern COBOL shops prefer GOBACK because it works correctly in both contexts.


17.10 The CANCEL Statement

The CANCEL statement releases a dynamically loaded subprogram from memory. The next time the subprogram is called, it will be reloaded from the load library, and its WORKING-STORAGE will be reinitialized to the values specified in VALUE clauses.

       CANCEL 'SUBPROG'

Or with a variable:

       CANCEL WS-PROGRAM-NAME

Why CANCEL Matters

Without CANCEL, a subprogram's WORKING-STORAGE persists between calls. This means:

  • Counters keep counting
  • Flags remain set
  • Accumulators retain their totals
  • File status indicators stay as they were

This persistence is often useful -- for example, a logging subprogram that maintains a count of how many entries have been written. But sometimes you need a fresh start. CANCEL provides that.

Behavior Summary

Scenario WORKING-STORAGE Behavior
First call to subprogram Initialized per VALUE clauses
Subsequent calls (no CANCEL) Retains values from previous call
Call after CANCEL Reinitialized per VALUE clauses
Program with IS INITIAL Always reinitialized on every call

See Example 5: CANCEL Statement for a working demonstration.

Restrictions

  • You cannot CANCEL a statically linked subprogram (it has no meaning since the subprogram is part of the load module).
  • You cannot CANCEL a program that is currently active (still on the call stack).
  • CANCEL has no effect on a program that has not been loaded.

17.11 Initial State and the IS INITIAL Clause

WORKING-STORAGE Persistence

By default, a subprogram's WORKING-STORAGE values persist across multiple calls. This is a fundamental characteristic of COBOL subprograms and a common source of both useful behavior and subtle bugs.

Consider a subprogram with this WORKING-STORAGE:

       WORKING-STORAGE SECTION.
       01  WS-CALL-COUNT    PIC 9(06) VALUE 0.
       01  WS-LAST-ERROR    PIC X(50) VALUE SPACES.

On the first call, WS-CALL-COUNT is 0 and WS-LAST-ERROR is spaces. If the subprogram increments WS-CALL-COUNT and moves an error message to WS-LAST-ERROR, those values will still be there on the second call.

IS INITIAL Clause

If you want a subprogram to start fresh on every call -- as if it were being loaded for the first time -- use the IS INITIAL clause on PROGRAM-ID:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. MYSUB IS INITIAL.

With IS INITIAL: - WORKING-STORAGE is reinitialized per VALUE clauses on every call - There is a performance cost (reinitialization takes time) - You do not need to CANCEL the program to reset its state

When to use IS INITIAL: - When the subprogram must have no "memory" of previous calls - When persistent state would cause bugs (e.g., accumulator that should be fresh each time) - When you want guaranteed predictable behavior regardless of calling sequence


17.12 Nested Programs (COBOL-85)

COBOL-85 introduced nested programs (also called contained programs), which allow you to define multiple programs within a single source file. Nested programs are enclosed between the parent program's IDENTIFICATION DIVISION and its END PROGRAM marker.

Basic Structure

       IDENTIFICATION DIVISION.
       PROGRAM-ID. MAINPROG.
       ...
       PROCEDURE DIVISION.
           CALL 'SUBPROG1' USING WS-DATA
           STOP RUN.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. SUBPROG1.
       ...
       PROCEDURE DIVISION USING LS-DATA.
           ...
           GOBACK.
       END PROGRAM SUBPROG1.

       END PROGRAM MAINPROG.

Visibility Rules

By default, a nested program can only be called by its immediately containing (parent) program. Sibling programs cannot call each other unless one of them has the COMMON clause.

The COMMON Clause

The COMMON clause on PROGRAM-ID makes a nested program callable by its sibling programs, not just the parent:

       PROGRAM-ID. LOGENTRY IS COMMON.

With COMMON, any program contained within the same parent can call LOGENTRY. This is useful for utility subprograms (logging, error handling) that multiple siblings need to access.

The GLOBAL Clause

The GLOBAL clause on a data item in the parent program makes that item visible to all contained programs without passing it as a parameter:

      * In the parent program:
       01  WS-ERROR-COUNT  PIC 9(06) VALUE 0 IS GLOBAL.

Any nested program can reference WS-ERROR-COUNT directly. This provides a form of shared state without explicit parameter passing.

Use GLOBAL sparingly: While convenient, GLOBAL data can create tight coupling between programs and make it harder to reuse nested programs in other contexts. Prefer explicit parameter passing when practical.

See Example 6: Nested Programs for a complete demonstration of END PROGRAM, COMMON, and GLOBAL.

Advantages of Nested Programs

  • Single compilation unit: All programs compiled together; no separate link step for nested programs
  • Encapsulation: Internal helper programs are hidden from the outside world
  • GLOBAL data: Controlled data sharing without parameter passing
  • COMMON programs: Cross-sibling access to utility routines

Limitations

  • Cannot be independently compiled or replaced
  • All changes require recompiling the entire source file
  • Less reusable than external subprograms (tied to their containing program)
  • Source file can become very large

17.13 Recursive Programs (COBOL 2002+)

COBOL 2002 introduced the ability to write recursive programs -- programs that call themselves either directly or indirectly. To enable recursion, the PROGRAM-ID must include the RECURSIVE attribute:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. FACTORIAL IS RECURSIVE.

How Recursion Works in COBOL

With the RECURSIVE attribute, each invocation of the program gets its own copy of LOCAL-STORAGE SECTION data. Note that WORKING-STORAGE is shared across all invocations (it is allocated once), while LOCAL-STORAGE is unique to each invocation:

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-SHARED-COUNT    PIC 9(04) VALUE 0.

       LOCAL-STORAGE SECTION.
       01  LS-LOCAL-DATA      PIC 9(10) VALUE 0.

Factorial Example

       IDENTIFICATION DIVISION.
       PROGRAM-ID. FACTORIAL IS RECURSIVE.

       DATA DIVISION.
       LOCAL-STORAGE SECTION.
       01  LS-N               PIC 9(04).
       01  LS-RESULT          PIC 9(18).
       01  LS-SUB-RESULT      PIC 9(18).

       LINKAGE SECTION.
       01  LK-INPUT           PIC 9(04).
       01  LK-OUTPUT          PIC 9(18).

       PROCEDURE DIVISION USING LK-INPUT LK-OUTPUT.
           MOVE LK-INPUT TO LS-N
           IF LS-N <= 1
               MOVE 1 TO LK-OUTPUT
           ELSE
               SUBTRACT 1 FROM LS-N GIVING LS-N
               CALL 'FACTORIAL' USING LS-N LS-SUB-RESULT
               ADD 1 TO LS-N
               COMPUTE LK-OUTPUT = LS-N * LS-SUB-RESULT
           END-IF
           GOBACK.

Caution: Recursion is rarely used in production COBOL. Most business problems are better solved with iterative approaches. Use recursion only when the problem is inherently recursive (tree traversal, hierarchical data processing) and the depth is bounded.


17.14 Calling Conventions and Parameter Alignment

When calling between COBOL programs, the compiler handles parameter alignment and matching automatically. However, there are important conventions to follow:

Group Items vs. Elementary Items

Passing a group item is generally preferred over passing many elementary items:

      * PREFERRED: Pass a group item
       01  WS-EMPLOYEE-RECORD.
           05  WS-EMP-ID       PIC X(06).
           05  WS-EMP-NAME     PIC X(30).
           05  WS-EMP-SALARY   PIC 9(07)V99.

       CALL 'PROCESS' USING WS-EMPLOYEE-RECORD

      * LESS PREFERRED: Pass individual fields
       CALL 'PROCESS' USING WS-EMP-ID
                             WS-EMP-NAME
                             WS-EMP-SALARY

Passing group items: - Reduces the number of parameters (fewer things to match) - Makes it easier to add fields without changing every CALL statement - Encourages defining interface copybooks (see Section 17.18)

Parameter Size Matching

The compiler does not verify that the caller's parameter sizes match the callee's LINKAGE SECTION sizes. This is the programmer's responsibility. A mismatch can cause: - Truncation: If the callee's item is shorter, it only sees part of the data - Overrun: If the callee's item is longer, it reads past the end of the caller's data (potentially reading garbage or causing an abend)

Best practice: Use copybooks to define the parameter structures and include the same copybook in both the calling and called programs:

      * In both programs:
       COPY EMPPARMS.

17.15 Interface to Other Languages

COBOL programs can call subprograms written in other languages, and other languages can call COBOL subprograms. The most common cross-language interactions on the mainframe are with C and Assembler.

Calling C Functions from COBOL

To call a C function, use BY VALUE for parameters that C expects as values:

       01  WS-INPUT-VAL    PIC S9(09) COMP-5.
       01  WS-RESULT       PIC S9(09) COMP-5.

       CALL 'myfunction' USING BY VALUE WS-INPUT-VAL
                          RETURNING WS-RESULT

Important considerations for C interoperability: - C expects parameters BY VALUE by default; COBOL's default is BY REFERENCE - Use COMP-5 (native binary) for integer parameters to match C's int - String parameters should be null-terminated when calling C (append X'00') - C function names are case-sensitive; COBOL typically folds to uppercase

Calling Assembler Routines

Assembler routines are called like any other subprogram:

       CALL 'ASMUTIL' USING WS-PARM-LIST

The assembler routine must follow the standard OS linkage conventions (save registers, establish addressability, restore registers). Parameters are passed BY REFERENCE by default, which matches the standard addressing convention.


17.16 Compilation and Linking

IBM z/OS: Separate Compilation

On IBM mainframes, COBOL programs are compiled individually and then linked together. The standard process involves:

  1. Compile: The COBOL compiler (IGYCRCTL) produces an object module
  2. Link-edit: The linkage editor (IEWL) combines object modules into a load module
  3. Execute: The load module is run

For static calls, all object modules are included in a single link-edit step:

//SYSLIN   DD  *
  INCLUDE MAINOBJ
  INCLUDE SUBOBJ
  ENTRY MAINPROG
  NAME MAINPROG(R)
/*

For dynamic calls, each subprogram is linked into its own load module and placed in a load library:

//SYSLMOD  DD  DSN=MY.LOADLIB(SUBPROG),DISP=SHR

IBM Cataloged Procedures

IBM provides cataloged procedures that simplify the compile-link-go process:

Procedure Steps
IGYWC Compile only
IGYWCL Compile and link-edit
IGYWCLG Compile, link-edit, and go (execute)
IGYWCG Compile and go (no explicit link step)

See Example 1 JCL and Example 2 JCL for complete JCL for static and dynamic call scenarios.

GnuCOBOL: Compiling on Open Systems

GnuCOBOL provides a simpler compilation model:

Compile and link a main program with subprograms (static):

cobc -x -o myprog main.cob sub1.cob sub2.cob

The -x flag creates an executable. All source files are compiled and linked together.

Compile subprograms as dynamically loadable modules:

cobc -m sub1.cob    # Produces sub1.so (Linux) or sub1.dll (Windows)
cobc -m sub2.cob    # Produces sub2.so or sub2.dll
cobc -x main.cob    # Main program only

The -m flag creates a dynamically loadable module. At runtime, GnuCOBOL searches for the module in the current directory, COB_LIBRARY_PATH, or other configured locations.

Compile as a static library:

cobc -c sub1.cob sub2.cob     # Produces .o object files
ar rcs libmysubs.a sub1.o sub2.o
cobc -x main.cob -L. -lmysubs # Link against the library

17.17 Common Subprogram Patterns

Validation Subprogram

A validation subprogram checks input data and returns a status code:

       PROCEDURE DIVISION USING LS-INPUT-DATA
                                LS-ERROR-TABLE
                                LS-ERROR-COUNT.
       0000-VALIDATE.
           MOVE 0 TO LS-ERROR-COUNT
           PERFORM 1000-CHECK-REQUIRED-FIELDS
           PERFORM 2000-CHECK-DATA-RANGES
           PERFORM 3000-CHECK-CROSS-FIELD-RULES
           GOBACK.

Date Calculation Subprogram

Date subprograms are among the most commonly reused components:

      * Calculate days between two dates
       PROCEDURE DIVISION USING LS-START-DATE
                                LS-END-DATE
                                LS-DAYS-BETWEEN
                                LS-STATUS.
       0000-CALC-DAYS.
           COMPUTE LS-DAYS-BETWEEN =
               FUNCTION INTEGER-OF-DATE(LS-END-DATE) -
               FUNCTION INTEGER-OF-DATE(LS-START-DATE)
           GOBACK.

Error Handler Subprogram

A centralized error handler receives error information and performs consistent handling:

       PROCEDURE DIVISION USING LS-ERROR-INFO.
       0000-HANDLE-ERROR.
           PERFORM 1000-LOG-ERROR
           PERFORM 2000-FORMAT-MESSAGE
           EVALUATE LS-ERR-SEVERITY
               WHEN 'W' CONTINUE
               WHEN 'E' PERFORM 3000-NOTIFY-OPERATOR
               WHEN 'C' PERFORM 4000-EMERGENCY-SHUTDOWN
           END-EVALUATE
           GOBACK.

Logging Subprogram

A logging subprogram provides consistent message formatting and output:

       PROCEDURE DIVISION USING LS-LOG-LEVEL
                                LS-PROGRAM-NAME
                                LS-MESSAGE-TEXT.
       0000-WRITE-LOG.
           MOVE FUNCTION CURRENT-DATE TO WS-TIMESTAMP
           STRING WS-FORMATTED-TIMESTAMP DELIMITED SIZE
                  ' [' DELIMITED SIZE
                  LS-LOG-LEVEL DELIMITED SPACES
                  '] ' DELIMITED SIZE
                  LS-PROGRAM-NAME DELIMITED SPACES
                  ': ' DELIMITED SIZE
                  LS-MESSAGE-TEXT DELIMITED SPACES
               INTO WS-LOG-LINE
           WRITE LOG-RECORD FROM WS-LOG-LINE
           GOBACK.

17.18 Designing a Subprogram Interface

Good subprogram design starts with a well-defined interface. Here are the principles that lead to maintainable, reusable subprograms.

Use Copybooks for Parameter Definitions

Define the LINKAGE SECTION structure in a copybook and include it in both the calling and called programs:

      * Copybook: DATEPARM.cpy
       01  DATE-VALIDATION-PARMS.
           05  DVP-INPUT-DATE      PIC X(10).
           05  DVP-DATE-FORMAT     PIC X(10).
           05  DVP-OUTPUT-JULIAN   PIC 9(07).
           05  DVP-DAY-OF-WEEK    PIC 9(01).
           05  DVP-RETURN-CODE    PIC 9(02).
           05  DVP-ERROR-MESSAGE  PIC X(50).
      * In the calling program's WORKING-STORAGE:
       COPY DATEPARM.

      * In the called program's LINKAGE SECTION:
       COPY DATEPARM.

This ensures perfect alignment and makes the interface self-documenting.

Parameter Design Guidelines

  1. Include a return code: Every subprogram should return a status indicator so the caller knows whether the operation succeeded.

  2. Include an error message: A human-readable error message is invaluable for debugging and operator communication.

  3. Use group items: Bundle related parameters into a single group item. This simplifies the CALL statement and makes it easier to add fields later.

  4. Separate input from output: Clearly delineate which parameters are input (the subprogram reads them) and which are output (the subprogram writes them). Consider using BY CONTENT for input parameters to enforce this.

  5. Document the interface: Comment the copybook with the purpose of each field, valid values, and expected behavior.

  6. Version the interface: If you need to change a parameter structure, consider adding a version field so the subprogram can handle both old and new callers during transition.


17.19 Best Practices for Modular COBOL Design

1. One Function per Subprogram

Each subprogram should do one thing and do it well. A subprogram named "VALIDATE-DATE" should validate dates and nothing else. If you find yourself adding transaction processing logic to a date validator, it is time to create a new subprogram.

2. Keep Parameter Lists Short

If you need more than five or six parameters, bundle them into a group item. Long parameter lists are error-prone and hard to maintain.

3. Always Return a Status Code

The caller needs to know whether the subprogram succeeded. Return 0 for success and nonzero values for specific error conditions. Document what each code means.

4. Use GOBACK, Never STOP RUN

STOP RUN in a subprogram terminates the entire run unit. GOBACK returns control properly. Always use GOBACK in subprograms.

5. Be Careful with WORKING-STORAGE Persistence

Remember that WORKING-STORAGE values persist between calls (unless CANCEL is used or IS INITIAL is specified). Initialize variables at the start of each invocation if they should not carry over:

       0000-PROCESS.
           INITIALIZE WS-WORK-AREAS
           ...

6. Handle the ON EXCEPTION Condition

Dynamic calls can fail if the subprogram is not found. Always code ON EXCEPTION for dynamic calls:

       CALL WS-PROG-NAME USING WS-PARMS
           ON EXCEPTION
               MOVE 'Program not found' TO WS-ERROR-MSG
       END-CALL

7. Match Parameter Descriptions Exactly

Use the same copybook in both the caller and the callee. If the descriptions do not match, you will get incorrect data, truncation, or storage overlays.

8. Prefer BY CONTENT for Input Parameters

If a parameter is input-only (the subprogram should not modify it), pass it BY CONTENT. This protects the caller's data and makes the intent clear.

9. Test Subprograms Independently

Write a simple test driver (main program) for each subprogram. Test it with valid data, boundary values, and invalid data before integrating it into the full system.

10. Document the Interface Contract

For each subprogram, document: - The purpose and function - Each parameter: name, type, direction (input/output/both), valid values - Return codes and their meanings - Any side effects (file I/O, database updates) - Performance characteristics (is it called once or millions of times?)


17.20 Putting It All Together: Complete Static Call Example

Let us walk through a complete example of a main program calling a subprogram using a static CALL.

The Main Program (STATICMN)

The main program prepares employee data, calls the payroll calculation subprogram, and displays the results:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. STATICMN.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-EMPLOYEE-RECORD.
           05  WS-EMP-ID              PIC X(06).
           05  WS-EMP-NAME            PIC X(30).
           05  WS-EMP-SALARY          PIC 9(07)V99.
           05  WS-EMP-TAX-RATE        PIC 9V9(04).
       01  WS-CALCULATED-FIELDS.
           05  WS-GROSS-PAY           PIC 9(07)V99.
           05  WS-TAX-AMOUNT          PIC 9(07)V99.
           05  WS-NET-PAY             PIC 9(07)V99.
       01  WS-RETURN-STATUS           PIC 9(02).

       PROCEDURE DIVISION.
           ...
           CALL 'PAYCALC' USING WS-EMPLOYEE-RECORD
                                 WS-CALCULATED-FIELDS
                                 WS-RETURN-STATUS
           ...

The Subprogram (PAYCALC)

The subprogram receives the employee data through the LINKAGE SECTION, performs calculations, and returns results:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PAYCALC.

       DATA DIVISION.
       LINKAGE SECTION.
       01  LS-EMPLOYEE-RECORD.
           05  LS-EMP-ID             PIC X(06).
           05  LS-EMP-NAME           PIC X(30).
           05  LS-EMP-SALARY         PIC 9(07)V99.
           05  LS-EMP-TAX-RATE       PIC 9V9(04).
       01  LS-CALCULATED-FIELDS.
           05  LS-GROSS-PAY          PIC 9(07)V99.
           05  LS-TAX-AMOUNT         PIC 9(07)V99.
           05  LS-NET-PAY            PIC 9(07)V99.
       01  LS-RETURN-STATUS          PIC 9(02).

       PROCEDURE DIVISION USING LS-EMPLOYEE-RECORD
                                LS-CALCULATED-FIELDS
                                LS-RETURN-STATUS.
           ...
           COMPUTE LS-TAX-AMOUNT =
               LS-GROSS-PAY * LS-EMP-TAX-RATE
           COMPUTE LS-NET-PAY =
               LS-GROSS-PAY - LS-TAX-AMOUNT
           MOVE 00 TO LS-RETURN-STATUS
           GOBACK.

The JCL

The JCL compiles both programs, links them together, and runs the result:

//* Compile main program
//COBMAIN  EXEC PGM=IGYCRCTL,PARM='NODYNAM,LIB,OBJECT'
//SYSIN    DD  DSN=USERID.COBOL.SOURCE(STATICMN),DISP=SHR
//* Compile subprogram
//COBSUB   EXEC PGM=IGYCRCTL,PARM='NODYNAM,LIB,OBJECT'
//SYSIN    DD  DSN=USERID.COBOL.SOURCE(PAYCALC),DISP=SHR
//* Link both object modules together
//LKED     EXEC PGM=IEWL
//SYSLIN   DD  *
  INCLUDE MAINOBJ
  INCLUDE SUBOBJ
  ENTRY STATICMN
/*
//* Run the linked program
//GO       EXEC PGM=STATICMN

See the full source code files: - example-01-static-call.cob -- Main program - example-01-subprog.cob -- Subprogram - example-01-static-call.jcl -- Complete JCL


17.21 Complete Dynamic Call Example

The dynamic call example demonstrates a transaction processing system where different subprograms are selected at runtime:

      * Determine which subprogram to call
       EVALUATE TRUE
           WHEN WS-TRANS-DEPOSIT
           WHEN WS-TRANS-WITHDRAW
               MOVE 'SUBPRGA' TO WS-PROG-NAME
           WHEN WS-TRANS-INQUIRY
               MOVE 'SUBPRGB' TO WS-PROG-NAME
       END-EVALUATE

      * Dynamic call -- program loaded at runtime
       CALL WS-PROG-NAME USING WS-TRANSACTION-DATA
                                WS-RESULT-DATA

The key difference from static calls is visible in the JCL: each subprogram is linked into its own load module, and the runtime searches STEPLIB to find and load the requested program.

See: - example-02-dynamic-call.cob -- Main program - example-02-subprog-a.cob -- Deposit/withdrawal handler - example-02-subprog-b.cob -- Inquiry handler - example-02-dynamic-call.jcl -- Complete JCL


17.22 Expected Output

Example 1: Static Call Output

=========================================
PAYROLL CALCULATION RESULTS
=========================================
Employee ID:   E10045
Employee Name: JOHNSON, ROBERT M.
Gross Pay:       4,583.33
Tax Amount:      1,008.33
Net Pay:         3,575.00
=========================================

Example 3: Parameter Passing Modes Output

=============================================
PARAMETER PASSING MODES DEMONSTRATION
=============================================

--- BEFORE CALL ---
BY REFERENCE field: [REFERENCE-ORIGINAL  ]
BY CONTENT field:   [CONTENT-ORIGINAL    ]
BY VALUE field:     [00000042]

  (Inside subprogram SUBPARM)
  Received BY REFERENCE: [REFERENCE-ORIGINAL  ]
  Received BY CONTENT:   [CONTENT-ORIGINAL    ]
  Received BY VALUE:     [00000042]
  After modifications inside subprogram:
  REF field is now:  [MODIFIED-BY-SUB     ]
  CONT field is now: [MODIFIED-CONTENT    ]
  VAL field is now:  [00000142]

--- AFTER CALL ---
BY REFERENCE field: [MODIFIED-BY-SUB     ]   <-- CHANGED
BY CONTENT field:   [CONTENT-ORIGINAL    ]   <-- UNCHANGED
BY VALUE field:     [00000042]               <-- UNCHANGED

Message from sub:   [Subprogram completed all modifications]

Example 5: CANCEL Statement Output

=============================================
CANCEL STATEMENT DEMONSTRATION
=============================================

--- CALLS WITHOUT CANCEL ---
(Counter in subprogram persists between calls)

Call #1: subprogram counter = 000001
Call #2: subprogram counter = 000002
Call #3: subprogram counter = 000003

--- CANCEL issued ---
Subprogram COUNTPGM has been canceled.

--- CALLS AFTER CANCEL ---
(Counter resets because WORKING-STORAGE was
 reinitialized when the program was reloaded)

Call #4: subprogram counter = 000001
Call #5: subprogram counter = 000002
Call #6: subprogram counter = 000003

--- ONE MORE CALL WITHOUT CANCEL ---
Call #7: subprogram counter = 000004

Notice: After CANCEL, the counter reset to 1.
Without CANCEL, it continued incrementing.
=============================================

Summary

Subprograms are the foundation of modular COBOL programming. The CALL statement, combined with the LINKAGE SECTION and PROCEDURE DIVISION USING clause, provides a clean mechanism for decomposing large programs into manageable, reusable components.

Key concepts from this chapter:

  • Static calls resolve at link time, producing larger but faster load modules
  • Dynamic calls resolve at runtime, offering flexibility and memory efficiency
  • BY REFERENCE shares memory; BY CONTENT protects the caller; BY VALUE passes stack values
  • RETURN-CODE and RETURNING provide status communication
  • GOBACK (not STOP RUN) is the correct exit from a subprogram
  • CANCEL releases dynamically loaded programs and resets their WORKING-STORAGE
  • Nested programs provide contained, single-compilation-unit modularity
  • Copybooks for parameter definitions ensure caller-callee alignment
  • Good interface design -- return codes, error messages, grouped parameters -- is the key to successful modular architecture

Mastering these concepts will enable you to write COBOL systems that are maintainable, testable, and built from reusable components -- exactly the qualities that have kept COBOL systems running reliably for decades.