Ask most programmers what "exception handling" means and they'll describe try/catch blocks, the vocabulary that Java, Python, and C++ all share, and that didn't really standardize across mainstream languages until the 1990s. PL/I had a working version of the same idea in 1964, in a language built for batch jobs submitted on punch cards, running on machines where the programmer who wrote the code usually wasn't even in the room when it ran. That's the detail worth sitting with before anything else: PL/I's condition-handling model wasn't a response to interactive computing, or a reaction to some later list of exception-handling best practices. It predates the entire premise those best practices were built around, by decades.
What used to happen when something went wrong
Before PL/I, a program that hit an unexpected condition, a division by zero, an array subscript outside its bounds, generally just stopped. Wikipedia's own summary of the language's design puts the batch-era reality plainly: PL/I programs ran with no possibility of intervention from a programmer sitting at a terminal, and an exceptional condition like division by zero would simply abort the program, leaving behind a hexadecimal core dump for someone to pick through later, offline, after the fact.
“An exceptional condition such as division by zero would abort the program yielding only a hexadecimal core dump. PL/I exception handling, via ON-units, allowed the program to stay in control in the face of hardware or operating system exceptions.”
Wikipedia, "PL/I" — ON-units and exception handling
IBM's answer was the ON-unit: a statement, or a whole block of statements, written in advance to run automatically the moment a specific named condition is raised, instead of the operating system just killing the job. That's a genuinely different model from what most languages of the era offered, and it's close enough to what later languages would call structured exception handling that the resemblance isn't a coincidence. It's the same problem, solved decades earlier, for a machine that had no way to ask the programmer what to do next.
A catalog with a number stamped on every entry
PL/I groups its conditions into three rough families. Computational conditions cover arithmetic and data movement going wrong: CONVERSION, FIXEDOVERFLOW, OVERFLOW, SIZE, STRINGRANGE, STRINGSIZE, SUBSCRIPTRANGE, UNDERFLOW, ZERODIVIDE. Input/output conditions cover files behaving unexpectedly: ENDFILE, ENDPAGE, KEY, NAME, RECORD, TRANSMIT, UNDEFINEDFILE. A third, smaller group, AREA, CONDITION, ERROR, and FINISH, covers everything else, including the catch-all ERROR condition that fires when nothing more specific was ever set up to handle what just happened. Every one of them, in IBM's actual runtime, comes with a numeric ONCODE you can inspect from inside the handler, which is the detail that turns "something went wrong" into an actual diagnosis instead of a guess.
- ONCODE 320, ZERODIVIDE: the program attempted to divide by zero.
- ONCODE 340, SIZE: an arithmetic assignment produced a value that doesn't fit in the target variable's declared size.
- ONCODE 310, FIXEDOVERFLOW: a fixed-point arithmetic result exceeded the precision defined for the field receiving it.
- ONCODE 350, STRINGRANGE: a SUBSTR reference asked for a position or length outside the actual bounds of the string.
- ONCODE 520, SUBSCRIPTRANGE: an array subscript evaluated to a value outside the array's declared bounds.
- ONCODE 612, CONVERSION: a data conversion failed, most often a character value that couldn't become the arithmetic or bit-string type the receiving field required.
- ONCODE 150, STRINGSIZE: an assignment truncated a string because the source was longer than the target's declared maximum length.
- ONCODE 9, ERROR: the catch-all. Either SIGNAL ERROR ran directly, or some other condition was raised with no more specific ON-unit in place to catch it.
ON-units inherit down the call chain
The part of this model that trips people up isn't any single condition, it's the scoping. ON-units are inherited down the call chain: when a block, a procedure, or even another ON-unit gets activated, it inherits whatever ON-units the code that called it had already established, unless it explicitly overrides them. A handler set up three procedure calls up the stack is still live, silently, in code that never mentions it. That's powerful when it's intentional, a shop can centralize its error handling in one place near the top of a program and trust it to catch trouble anywhere underneath. It's also exactly the kind of behavior that makes a maintenance programmer's life harder when they're reading a routine in isolation and can't tell, from that routine alone, which ON-unit will actually fire when something breaks.
Two statements exist specifically to manage that inheritance. SIGNAL lets a programmer manually raise any condition on demand, which turns out to matter less for production logic than for testing: it's how you deliberately exercise an ON-unit during development without having to construct real bad data to trigger it. REVERT does the opposite job, restoring whatever ON-unit was in effect before the current one was established, so a block of code can install a temporary handler, do its work, and hand control back to the handler that was already there, without permanently altering the condition environment for the rest of the program.
Conditions a programmer invents
The catalog above covers exactly the conditions IBM's runtime raises on its own. PL/I doesn't stop there. The CONDITION class lets a programmer declare an entirely new, named condition that doesn't correspond to any hardware or runtime event at all, then raise it deliberately with SIGNAL and catch it with an ON-unit exactly like any built-in one. That's a genuine ancestor of the custom exception classes object-oriented languages would make routine decades later, present in a language shipping in 1966, well before "define your own exception type" was a pattern anyone had a name for. A PL/I program can use the identical mechanism that catches ZERODIVIDE to signal an application-specific validation failure, and handle both through the same syntax.
A condition for the end of the program, not just the middle of it
One more condition worth calling out on its own is FINISH, raised automatically when a program is about to end normally. It gives an ON-unit a guaranteed last chance to run cleanup code, close files deliberately, or write a final log entry, regardless of which of the program's many possible exit paths actually got taken. That's the same job a "finally" block does in later languages, solved with the identical mechanism PL/I already used for genuine errors, one more entry in the same condition list rather than a separate keyword and a separate concept bolted on afterward.
Not every condition lets the program come back
The detail that separates PL/I's condition model from a simple error flag is what's allowed to happen after the handler runs. Some conditions, STRINGRANGE and UNDERFLOW among them, permit the program to resume at the exact point where the interruption occurred once the ON-unit finishes, as if nothing happened, provided the handler fixed whatever was wrong. Others don't get that option. SUBSCRIPTRANGE is one of them: once it's raised, the program can't simply patch the subscript and pick back up where it left off. The condition escalates to ERROR instead, and ERROR's default action, if nothing catches it, is to terminate the task. Writing an ON-unit for SUBSCRIPTRANGE means writing something that decides what to do next, not something that quietly fixes an index and continues, because continuing from the exact instruction that failed was never on the table for that condition to begin with.
A safety check you can turn off with a comma
PL/I also lets a programmer disable specific checks at the level of a single statement, using what the language calls condition prefixes: writing (NOSUBSCRIPTRANGE) or (SIZE, NOSTRINGRANGE) immediately before a statement changes which of these checks the compiler actually enforces for that one line, regardless of what's set up anywhere else in the program. That's a real tradeoff exposed directly in the syntax, not buried in a compiler flag or a build configuration file somewhere else entirely: a programmer can decide, statement by statement, that a particular bounds check costs more in performance than it's worth on a hot path, and say so right there in the code, where the next person to read it can see the decision was made on purpose.
The one place this model still bites people today
The category distinction between conditions that resume and conditions that don't isn't just an academic point. It's also where PL/I's condition system has produced a real, documented migration hazard. IBM's own support organization tracked a defect, APAR PI56511, in which a SUBSCRIPTRANGE condition raised by code that originated under the older PL/I for MVS & VM compiler wasn't consistently caught by an ON SUBSCRIPTRANGE unit after that same code was recompiled under Enterprise PL/I. A bounds check that had worked correctly, unattended, for years, could stop firing the moment a shop upgraded compilers, and nothing about the source code itself had to change for that to happen.
The practical lesson is specific enough to act on: if a bounds check that used to fire suddenly goes quiet right after a compiler migration, the ON-unit itself is probably not the bug. The gap is upstream of it, in exactly the kind of interaction between compiler version and runtime behavior that a design built around inherited, block-scoped condition handlers makes possible in the first place. It's a strange thing to be able to say about a mechanism designed in 1964, but it is still, in 2026, one of the single most concrete things a working PL/I programmer needs to know about ON-conditions beyond what the syntax itself teaches them.
Read the full ONCODE 520 SUBSCRIPTRANGE breakdown →IBM is still funding work on this exact boundary
None of this is theoretical upkeep on a museum piece. IBM shipped Enterprise PL/I for z/OS 6.2, available through Shopz starting June 13, 2025, with improved compiler warnings specifically aimed at catching storage-related issues before runtime, the same broad category of problem that SIZE, STRINGSIZE, and SUBSCRIPTRANGE exist to catch after the fact. That's a compiler team spending engineering time moving part of the condition system's job earlier, from something an ON-unit discovers while the program is already running to something the compiler can flag before the program ever ships. Nobody does that kind of unglamorous, unmarketable work on a language they expect to stop supporting.
“reaffirms its long-standing commitment to PL/I on z/OS, built with over 60 years of expertise”
IBM, Enterprise PL/I for z/OS 6.2 announcement, via Premkumar Swaminathan, IBM Z community blog, April 2025
That framing is worth taking at face value rather than as boilerplate. A vendor doesn't need to say "60 years of expertise" about a product it's quietly sunsetting; it says that about a product it expects to still be answering support tickets for. Sixty-plus years after ON-units first let a batch program survive its own arithmetic mistakes, the compiler underneath them is still getting new warnings, new architecture targeting, and a version number that keeps climbing.
Where this leaves you
- PL/I's ON-units are a genuine structured exception-handling model, built in 1964 for a batch-processing world with no way to intervene once a job started running.
- Conditions split into computational, I/O, and a handful of catch-all categories, each with a numeric ONCODE the runtime hands the handler for diagnosis.
- ON-units inherit down the call chain by default, which centralizes error handling well but can make it genuinely hard to tell, from a single routine, which handler will actually fire.
- Not every condition allows the program to resume where it left off; SUBSCRIPTRANGE forces escalation to ERROR, while STRINGRANGE and UNDERFLOW permit a return to the interrupted statement.
- A documented IBM defect, APAR PI56511, shows SUBSCRIPTRANGE handling can silently stop working across a compiler migration, the sharpest real edge case in this whole model.
- Enterprise PL/I 6.2, shipped mid-2025 with z17 support, is current evidence IBM is still investing engineering time in exactly this boundary between compile-time and run-time safety.