BankLang

Error handling

One page for every way a generated program can report that something went wrong, and how on failure maps onto each. It was spread across four documents.

The rule underneath all of it: a run that did not do what it was submitted to do must not end the way one that did ends. Every finding in the 2026-08-05 audit's P1 section was a violation of that single sentence.


The return-code contract

A batch step ends with one of these, and the next step's COND= is written against them.

Code Meaning
0 The work completed.
4 The program set it deliberately with returnCode = 4. Nothing the compiler generates uses 4.
8 Likewise. Reserved for a program that wants a warning code.
12 A failure the program named. BANK-FAILURE-CODE says which.
16 A sort or merge did not complete. SORT-RETURN says so.

Under CICS none of it applies: RETURN-CODE is a batch step's answer to JCL and nothing in a region reads it. A CICS program that has failed abends with code BLNG, which is what backs out the unit of work.

Two registers, not one

       01  BANK-FAILURE-CODE    PIC X(32) EXTERNAL.
       01  BANK-RETURN-CODE     PIC S9(4) COMP EXTERNAL.

RETURN-CODE cannot hold the answer while the program is still running. The Language Reference is explicit: "the RETURN-CODE special register in the calling program is set to the value of the RETURN-CODE special register in the called program" — so every generated transaction, which ends by calling BANKAUDT, used to hand the operating system the audit program's zero.

BANK-RETURN-CODE holds it and BANK-MAIN moves it out once, after everything the program performs has returned.

BANK-FAILURE-CODE names the failure, so a job log says which of a dozen guards fired. Both are EXTERNAL because a recursive or nested function is a separate program rather than a paragraph, and a failure raised inside one has to reach the caller.

The one failure path

Every generated failure does the same three things, in the same order:

               MOVE 12 TO BANK-RETURN-CODE
               MOVE "READ-FAILED" TO BANK-FAILURE-CODE
               GO TO POST-ACCOUNTS-EXIT

Set the code, name the failure, leave through the enclosing routine's exit. Control returns to BANK-MAIN, which is the only paragraph that ends the program. Where there is no enclosing routine — a file error declarative, an XML handler section, both entered by the run time rather than performed — BANK-ABEND is the paragraph it goes to instead.

Every call site tests the register afterwards:

           PERFORM ACCRUE THRU ACCRUE-EXIT
           IF BANK-FAILURE-CODE NOT = SPACES
               GO TO POST-ACCOUNTS-EXIT
           END-IF

Not only after a call to something declared able to raise. An overflow, a failed READ or a subscript outside its table is a failure the callee never declared, and checking only declared raises is how one used to run on regardless.


on failure

A transaction may declare a handler:

entry transaction postAccounts(account: Account) {
  on failure {
    audit("REJECTED", account.idempotencyKey);
  }
  ...
}

It compiles to the standard COBOL shape for a body with an early exit — a wrapper that performs the body THRU its exit and then inspects the register:

       POST-ACCOUNTS.
           MOVE SPACES TO BANK-FAILURE-CODE
           PERFORM POST-ACCOUNTS-BODY THRU POST-ACCOUNTS-BODY-EXIT
           IF BANK-FAILURE-CODE NOT = SPACES
               PERFORM POST-ACCOUNTS-FAILURE THRU POST-ACCOUNTS-FAILURE-EXIT
           END-IF
           CONTINUE.

Every failure reaches it, whatever raised it. A transaction that posted to the ledger before failing gets a "ROLLBK" call to BANKLEDG ahead of the handler: the postings already made are not this program's to keep, and unwinding them is the ledger's job rather than compensating debits and credits of the compiler's invention.

A transaction with no handler still fails the step. It says so in the job log and ends 12, rather than the body stopping where it failed and the wrapper returning success.


File status

Every I/O statement's status is checked, not only OPEN. IBM's guidance is to "check the file status key after each input or output request", and a WRITE that filled the volume or a CLOSE that could not write its last buffer is exactly the failure nobody investigates until a reconciliation months later.

The status field carries condition names:

       01  ACCOUNT-INPUT-STATUS PIC X(2).
           88  ACCOUNT-INPUT-STATUS-OK  VALUE "00" THRU "09".
           88  ACCOUNT-INPUT-STATUS-EOF VALUE "10".
           88  ACCOUNT-INPUT-STATUS-DUPKEY VALUE "22".
           88  ACCOUNT-INPUT-STATUS-NOTFND VALUE "23".

"00" THRU "09" is IBM's successful-completion class. "02" is a duplicate alternate key where duplicates are allowed, "05" is an OPTIONAL file created on this run — a check written NOT = "00" stops a restartable batch on its first night.

The statuses a given statement is allowed to produce are its own. End of file on a read and a key that was not there on a keyed read are questions answered rather than files that failed, and the program branches on them:

               IF NOT ACCOUNT-INPUT-STATUS-OK AND NOT ACCOUNT-INPUT-STATUS-EOF
                   DISPLAY "READ FAILED accountInput STATUS "
                       ACCOUNT-INPUT-STATUS UPON SYSOUT
                   MOVE 12 TO BANK-RETURN-CODE
                   ...

A file may also declare an error handler, which becomes a USE AFTER STANDARD ERROR PROCEDURE in DECLARATIVES. That is entered by the run time rather than performed, so a failure inside it goes to BANK-ABEND.

Declaring no status field at all is BANK-FILE-001.


SQLCODE

+100 is the only "not found". Negative is an error: -911 is a deadlock the thread lost, -904 a resource that was not available, -805 a package that was never bound.

A body that runs SQL and never tests SQLCODE is BANK-SQL-001. A body that tests it but cannot separate an error from +100 is BANK-SQL-007 — because if sqlcode == 0 { found } else { not found } turns a deadlock into a customer being told their account does not exist, and sqlcode != 100 is no better, putting +100 and -911 on the same side.

  if sqlcode < 0 {
    reply.outcome = EnquiryOutcome.UNAVAILABLE_DB;
  } else {
    if sqlcode == 0 { ... } else { ... }
  }

A cursor loop fails the step on a negative SQLCODE rather than merely leaving, and closes the cursor on the way out so the failure does not hold locks. A loop that stopped on its declared bound with SQLCODE = 0 also fails: the result set was not finished.


CICS RESP

Every CICS command captures its response, and an unchecked one is BANK-CICS-001.

The comparison is against the condition name, not a number:

           IF LINK-RESP = DFHRESP(NORMAL)

The API Reference names one value a program may write — a normal return is DFHRESP(NORMAL) — and says the rest are tested "by means of DFHRESP", the translator's own built-in function. The numbers behind the other conditions belong to the translator, so comparing against one is BANK-CICS-004.

A CICS transaction that has failed abends with BLNG from BANK-MAIN, which backs out the unit of work. EXEC CICS RETURN is followed by GOBACK, as IBM's own sample writes it: ending the task is something CICS does, not something COBOL does.

A commarea shorter than the record is refused before anything reads it, with abend code BKNC — IBM's guidance is to verify the length "matches what the program expects", because a short one leaves the MOVE reading somebody else's storage.

The commarea is written back on both paths, including the one where the transaction failed and its on failure handler ran: a return code the handler sets and the caller never sees leaves a failure looking like a success. And a transaction that computes its answer into a record parameter that is not the commarea is refused outright with BANK-CICS-005, because there is no path from working storage back to a caller once the task has ended.


MQ

Every MQI call returns a completion code and a reason code, and both are tested and both are named in the message. Reporting one without the other leaves an operator with either "it failed" or a number with no context.

An empty queue is not a failure: MQRC-NO-MSG-AVAILABLE (2033) is how a batch finishes its work, and folding it in with the errors would stop the job every night. Folding it in with success would process the message area again, still holding the last message read.


IMS DL/I

The two characters DL/I leaves in the PCB are the whole error model, which is why the status field is required rather than optional: without it a getUnique that found nothing is indistinguishable from one that worked.


Loop bounds

Every loop carries one, and reaching it is a failure:

           IF POST-ACCOUNTS-LOOP-1 >= 1000000 AND
               (ACCOUNT-INPUT-STATUS = "00")
               DISPLAY "LOOP LIMIT 1000000 REACHED, WORK UNFINISHED"
                   UPON SYSOUT

The condition is re-evaluated so the two exits are told apart exactly. A loop that ended because its own condition went false is the ordinary end; the counter at the limit while the condition still holds is the bound stopping work that had not finished — which without this branch was a five-million-record master processing the first million and ending RC=0.


Arithmetic overflow

Any computation that can overflow carries ON SIZE ERROR. COBOL leaves the receiving field alone when the phrase is present rather than storing the truncated answer, which is what makes stopping safe: the wrong value never reaches the ledger.

Without it, the digits truncated are the high-order ones — so an overflowing addition does not produce a large wrong number that stands out, it produces a plausible small one.

Subscripts

See for-mainframe-engineers.md.



Read this page as Markdown on GitHub →