40

Why, usually, does 0 mean success in process return status codes?

When I worked at TANO Corp in New Orleans in the late 70s and early 80s, the convention there was the opposite: 1, true, was the "it's all OK", and 0, false, was "oops!"

Along all the years I've known about this "0 means success" idiom, I've asked people if they have any clue where it came from and the best answer was PURE speculation — admittedly — from the respondent: it means "nothing to report."

OK, that speculation makes sense to me. And a LOT of the origin of terms people ask questions about here in Retro have answers like "it was borrowed / carried over from non-computing practice." In this case, "nothing to report" is a commonly heard phrase in a military context and considering the early focus of modern computing.

1
  • Comments are not for extended discussion; this conversation has been moved to chat.
    – Chenmunka
    Aug 11, 2022 at 8:52

5 Answers 5

46

Here is a Multics document on "Standard Error Handling Practice" from March 1969.

enter image description here

Scroll down to the second page and find "by convention, zero is the code for normal completion".

enter image description here

That indicates to me that by the time of Multics' it was already common practice in computing that 0 indicated success/normal for subroutines. In Multics of course there was a close affinity between the way subroutines worked and the way processes worked - so it was natural that processes used the same mechanism. From there: UNIX.

By the way, consider the Multics "shell" - leveraging the remarkable Multics' property that you could call any entry point in any executable file in the entire file system as a subroutine (if you had permissions to it, of course) - it simply called all "commands" - system or user-written - with a normal subroutine call in machine language, and the dynamic linker handled the rest. That means that in Multics the return value from such a subroutine call became the return value that a command would check (and then it would call a system subroutine to correctly handle the failure from the command-line POV). From the same document:

enter image description here

The history of using 0 for success started a long time ago - most likely earlier than Multics. Though it probably wasn't the only such convention. If you want an opinion my guess is that, based on the experiences of people writing programs in "the early days" they recognized that there were frequently multiple reasons a given subroutine/system call/command could fail, and some of those different reasons might be interesting to the caller. (E.g., "device not ready" vs "no permission to use device".) On the other hand, there probably weren't a bunch of compelling examples for having multiple success codes on an API. And also they knew that the easiest, cheapest, simplest method of returning a result from a subroutine was just to leave a value in a particular distinguished register - that was a looooong time practice. And with that in mind you then look at the possibilities of singling out one particular in-band value of an integer, with a mind to making it simple and cheap to test for (by the caller) and maybe also simple and cheap to establish (by the callee) ... and 0 stands out.


BTW, how do you make any subroutine into an entry point of an executable? Simple! Multics allowed files to have multiple names and all you needed to do was add, to your file, the name of each subroutine you wanted to allow the dynamic linker to find. Especially for the system commands it was common to find an executable file with several, or a dozen, extra names attached to it - each one being a subroutine in that binary, and each one callable from the command line. The directory list command would list all the names of each file of course (which would look strange to modern eyes). It's a bit more convenient for the programmer than the modern technique of hard-linking the same file from multiple names and then having the code figure out which command was wanted by inspecting argv[0].

1
  • Comments are not for extended discussion; this conversation has been moved to chat.
    – Chenmunka
    Aug 11, 2022 at 8:52
27

TL;DR: CPUs handle Zero unique among all integers.

Zero is set apart from any other integer by the way ALUs work. On low level it is thus of advantage to use zero as return code for success, as it's the most easy to be detected. With that being said, it comes natural to extend this to process/program return codes.


Way Back in Time and Close to Hardware

Much meaning can be put in hindsight onto return codes, but such based on integers inherently benefit from integers being treated as first class member by next to all CPU architectures. This includes almost always a way for taking execution depending on an integer being zero or non zero. Either by offering a value based branch, or a fast, low cost test followed by a branch.

At that point its helpful to keep in mind, that error handling is a burden slowing down execution. A substantial one, considering that programs are all about calling functions - within and from the OS.

By assigning zero as default value for success this advantage can be used for cheap (*1) error/non-error detection, reducing error handling cost to a possible minimum.

Of course that argument might work either way, but when looking at ordinary execution, then functions will usually will have have to be way more differentiated why they failed than why they succeeded. Reserving one return code for success and MAX_INT-1 for error numbers does again simplify error handling. Of course a differentiated error handling with multiple fields and structure past a simple number will beat all of that - but also be an overkill in 99.999% of all cases.

And then there was C and Unix

While (early) mainframe OS used dedicated mechanics for success and return/error information, the designers of Unix were all about simplifying to the absolute minimum. Using zero to distinguish the most notable case, yields the best performance.

C/Unix was not only using the advantage of integers within programs (*2), but as well extended it to the shell. After all, a programs main() is also just a function, so why bother to convert this in any way but simply forwarding that value to shell?

The C Programming Language Second Edition mentioned this as general rule on p.27:

Typically, a return value of zero implies normal termination; non-zero values signal unusual or erroneous termination conditions.

Divide et Impera

What goes for integers works of course as well with signed integers. Those divide all none zero values in two (almost) equal sized sets, marked by the sign - a feature as easy to detect as like zero and as well privileged by many architectures.

By using signed integers Now not only reasons for being non successful, but as well reasons for success can be reportet. C does make as well use thereof (*3).

Two Halves of a Shell

(*4)

Despite process exit codes usually seen as unsigned integers, the sign principle got as well extended to shell use by reserving values of of 128 and above, like for the return value of a sub-process.

One Exit Code to Rule Them All

In batch programming 'success' is one most important 'message', as it's the one to be detected to carry on with whatever is next. Think of a very classic use case like processing data from a tape. Such a program may return beside the basic

  • Everything worked fine and
  • Generic fail

exit codes for

  • Wrong tape,
  • No tape assigned or
  • Add follow up tape

Depending on the environment the later may require the request of operator assistance to mount the right tape, a follow up tape or search an archive. All things the data process can and should not do on it's own, as it's heavy dependent on customer installation what the right handling will be.

Again the privilege of zero being special for all integer makes batch writing consistent, easy to do and most important, easy to read. Anyone who has worked in a (classic) data center will know how important a consistent structure of batch files is.


Long story short:

Zero is privileged as return value by hardware, assigning it to the most common case comes naturally


*1 - In a sense of compact code and fast execution.

*2 - Immortalised by the ubiquitous `if (rc) { /*errorhandling */ };

*3 - Of course it wouldn't be C if it doesn't get complicated at that point, for example with read() now only reserving -1 for some error and reporting the real error number in errno, adding several pitfalls in non trivial programs :)

*4 - In some ways the usage of signed integers and the resulting easy detection of positive and negative values and zero is much like the shell of a Bivalvia: Two valves connected by a hinge :))

1
  • Comments are not for extended discussion; this conversation has been moved to chat.
    – Chenmunka
    Aug 8, 2022 at 11:57
11

A process, or a system call, can have multiple outcomes, typically more than two. There could be several "successful" outcomes, and several "unsuccessful" ones.

When these outcomes are identified by numbers, choosing negative numbers to mean "unsuccessful", and non-negative numbers to mean "successful" is a fairly mnemonic choice, given the associated meanings of the words "positive" and "negative" in most fields (except, perhaps, some medical contexts where "negative" is often the desired result of a test).

With these conventions, finding out whether a result of an operation was favorable or unfavorable overall, on a machine with two's complement arithmetic would involve just one instruction, checking for the sign of the status value. Thus, 0 is not just "nothing to report", but rather, "successful, nothing more to report".

However, given that most applications have only one truly "successful" condition, in case of process return codes conventions may vary, and the negative range may now mean "terminated involuntarily", and the positive range may mean "terminated voluntarily" (which is at least a partial "success").

14
  • 2
    Also (IMO) it hasn't actually worked well to supply multiple success codes. E.g., Windows NT allows multiple success codes in the API. In the first place they are hard to use when provided, there is usually little reason to use them, and since most APIs don't have multiple success codes if you wanted to use them you'd have to look them up in the docs each time to see if your API had them. When they do exist it is easy to get them wrong by people forgetting to test for error/success using the proper macro (thereby turning warnings to errors). ....
    – davidbak
    Aug 7, 2022 at 15:44
  • ... Finally, in the case of process returns and the shell: they don't compose into pipelines or other shell constructs easily.
    – davidbak
    Aug 7, 2022 at 15:44
  • 1
    @davidbak - that people write code without reading documentation is surely a poor reason for restricting your API's ability to define more than one type of success. Mind you, I was trained on VAX/VMS, which had a machine instruction for "is this a success/fail status".. Aug 7, 2022 at 18:18
  • 4
    @another-dave - not really, if it concerns you that applications/libraries be written on your platform be reliable. Even from the consideration that if it is considered difficult/annoying to write reliable apps/libs on your platform it'll get a bad reputation. And as an app/lib writer I'd like my platform to be easy to write correct reliable code for, too.
    – davidbak
    Aug 7, 2022 at 18:37
  • 1
    @RichardT: I guess you've never looked at x86 assembly language and machine-code, then. Testing for non-zero is a 2-byte instruction, test eax,eax. Checking for exactly 1 is a 3-byte instruction, cmp eax,1. Also, returning zero is a 2-byte instruction, xor eax,eax, while returning any other integer value is normally done with a 5-byte mov eax, 1. Some other ISAs like ARM and MIPS can branch on a value being (non)zero in a single instruction (ARM cbz / cbnz, or MIPS beq $v0, $zero, target without needing to li a 1 into a register to compare with). Aug 10, 2022 at 19:52
9

Not an answer as to 'first', but since we're talking a lot about Unix, it might be useful to point out that Unix had at least 3 'error' conventions. I talk here about PDP-11 Unix.

  1. Syscall success/failure. Success indicated by carry bit clear, possible single-word return value in R0. Failure indicated by carry bit set, negative error code in R0. Distinguishing success from failure is a branch on carry, not a test of R0.

  2. C RTL success/failure. For C library routines wrapping kernel calls, the problem was mapping the two kernel outputs (C-bit, R0) into one function return. This was done by picking a value that was "not possible" for a success return, generally 0 or -1, and saying that was an error indication. The actual kernel error code was written to a global variable (negated, I am not sure why).

  3. Process exit status. The case of interest is of course what the parent process actually sees, which observation occurs via a 'wait' call. The result is a composite of the kernel's termination reason (exit, kill, segv, etc), a flag indicating whether core was dumped, and the 8-bit exit status from the process if it voluntarily exited.

So in Unix, the kernel only has one 'success' reason for termination - the process called 'exit', which is coded as zero (and there was no core dump in this case, so that flag is zero too). That leaves 8 bits for the process use.

Second-edition Unix man pages for the shell say nothing about the shell taking any particular action on zero/non-zero for the actual exit status; only about printing messages for the non-exit cases. I conclude there was not yet a strong convention in this regard. Perhaps it only becomes important when 'programmable' shells appear - maybe Programmer's Workbench Unix?

9
  • "negated, I am not sure why" — presumably, to easily check for error (since -errno < 0, just as -1). E.g. write(2) returns a signed number of bytes, whose being negative indicates an error. Dunno about the UNIX kernel, but Linux returns -errno on error, which is then written by the libc wrapper to the actual errno variable after negation, and the wrapper then returns -1.
    – Ruslan
    Aug 8, 2022 at 9:35
  • @ruslan - yes, Unix returns negative numbers for error codes, as I said. My question is, why not set those actual values in errno? Or, why would the kernel implementor and the C RTL implementor, who were presumably in very close communication, make opposite decisions? Your 'checking for error; sentence does not seem to hold water, since by intent, errno is only set by the RTL after an error has been reported. Aug 8, 2022 at 12:02
  • Because error codes are positive and couldn't be told from the actual return value. So the logic is like: if return value is negative, there's an error, negate the value to get error code. Otherwise it's the value corresponding to the purpose of the syscall: number of bytes read, file descriptor opened, etc. Maybe the error codes themselves could be made negative to avoid the need for negation, but that's another question why they weren't made negative.
    – Ruslan
    Aug 8, 2022 at 12:06
  • 1
    The point in (PDP-11) Unix is that the actual return value or error code from the kernel is separately conveyed from the success/fail indication, so there is never any occasion on which it is necessary to determine whether a particular set of bits in the return value means success or failure. Aug 8, 2022 at 12:14
  • @Ruslan: The negation sounds weird. Some modern systems like Linux use in-band signalling of errors in their system call ABI with -errno values, e.g. for Linux, any return value unsigned >= -4095ULL is an error code. (See the kernel/library differences note in getpriority(2)). But MacOS/Darwin signals error / non-error out-of-band in the carry flag, like another-dave is saying PDP-11 Unix did, with the return value register (rax on x86-64) holding an errno value if CF==1, otherwise a normal return value. Aug 10, 2022 at 20:07
1

Many CPUs have a "flags" register that contains a number of bits automatically set or reset according to the last ALU or load operation.

Commonly one of those bits is often a "Z" bit which is automatically set when a value is 0.

CPUs also typically have a "compare" instruction that does a subtraction, discards the result, but lets the flags stand.

However, if you want to test for zero, the compare is unnecessary since the Z bit will be set automatically from the last operation that loaded a zero value.

So if you need 2 values to decide between "error" and "not-error", then zero and non-zero are convenient because save an instruction/a couple cycles (valuable things in late 60's) on each system call.

I could be wrong ... I haven't really looked at this in depth yet, but just skimming over the Multics Processor Manual seems to suggest at least one of the CPUs Multics ran on worked the same way - looking at the lda instruction (page 32) for example, mentions something about an "indicator" being set if the value is 0, I believe. So the tnz (transfer on non-zero) instruction could be used immediately after the call to jump somewhere if there was a problem, without a cmpa instruction first.

Of course that manual has a copyright date of 1985, so not 100% sure.

2
  • Not all computer architectures set the flags on simple load immediate value to register operation, only some do. And some interfaces use the carry flag for indicating between success and failure.
    – Justme
    Aug 19, 2022 at 21:48
  • Absolutely. For example, CPUs with "skip" instructions that I think probably didn't have flag registers at all. What I read above led me to believe maybe the CPU architectures Multics ran on did work like that though.
    – LawrenceC
    Aug 22, 2022 at 20:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .