Skip to content
VLSI Mentor

C Part I · Module 12 · Standard Library, Files and Program Interfaces

Program Exit and Return Codes

The number in `return 1` has been appearing since Chapter 1.4 without anyone explaining it. It is the program's entire report to whatever invoked it: the only thing a shell script, a build system or a parent program can see of everything your program decided, compressed into one small integer. Small enough that it has a limit, which this chapter measures, and the limit has a consequence that reads like a joke until you see it happen: a program can report failure with a number large enough that the number wraps around to mean success. There is also more than one way to end a program, and the ways differ in what they do to the buffered data Chapter 12.4 measured sitting in memory — in one case by discarding all of it.

Intermediate18 min readCPart IFundamentalsStandard Library

Part I · Module 12 · Chapter 12.7 · Program Exit and Return Codes

1. The Status Is the Report

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int main(void) { ... return 0; }

main returns int because that value is the program's exit status — handed to whatever started the program, and the only part of the program's conclusions that survives it.

The standard defines exactly three values:

ValueMeaningFrom
0successthe language
EXIT_SUCCESSsuccess<stdlib.h>
EXIT_FAILUREfailure<stdlib.h>
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
EXIT_SUCCESS=0 EXIT_FAILURE=1

They are 0 and 1 here. EXIT_SUCCESS is not required to be 0 — it is required to mean success — and EXIT_FAILURE has no specified value at all. Use the macros when portability matters and 0 when it does not; 0 for success is itself guaranteed by the standard, so return 0 is always correct.

Everything else is a convention of the host. A program returning 2 for "file not found" and 3 for "bad argument" is using a scheme the host and its users agree on, not something C defines. That is normal and useful; it is worth knowing which part of it is the language and which is local custom.

2. The Status Is Eight Bits

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  return 0     -> shell saw 0
  return 1     -> shell saw 1
  return 2     -> shell saw 2
  return 42    -> shell saw 42
  return 255   -> shell saw 255
  return 256   -> shell saw 0
  return 257   -> shell saw 1
  return -1    -> shell saw 255

Read the last three rows.

return 256 reported success. The status is truncated to its low eight bits, 256 is 0x100, and the low eight bits of that are zero. A program that used 256 as an error code told its caller everything was fine.

return 257 reported 1, which is a different error than the one intended but at least still an error.

return -1 reported 255. Negative values wrap into the top of the range rather than being preserved.

An exit status has about 256 usable values, and only the low eight bits of what you return survive. This is not in the C standard — C says the value is returned "in an implementation-defined manner" — it is what hosted environments overwhelmingly do, and it is what was measured here.

The practical rules:

Keep status values in 0–125. Values above that collide with conventions many shells use for signals and special conditions.

Never compute a status from an error code that could be large — an errno value, a line number, a count. That is exactly how return 256 happens.

Zero means success and nothing else does. Do not return a count of items processed as the status; a successful run that processed 256 items would report success by coincidence and a successful run that processed 1 item would report failure.

3. Falling Off the End of main

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #include <stdio.h>
    int main(void) { printf("no return statement\n"); }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
no return statement
  exit status = 0

No warning, and the status is 0.

Since C99, reaching the closing brace of main without a return is equivalent to return 0;. main is the only function in C for which this is true — Chapter 5.4 established that falling off the end of any other non-void function and then using the result is undefined.

It is defined, and writing return 0; anyway is still better: it is one line, it makes the success path explicit, and it does not make a reader stop to remember whether this particular function is the special one.

4. Four Ways to End

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    return status;        /* from main */
    exit(status);         /* from anywhere */
    _Exit(status);        /* from anywhere */
    abort();              /* from anywhere */

They differ in exactly the way that matters after Chapter 12.4. A program that buffers 35 bytes to a file and some unflushed text to stdout, then ends each way:

Four ways to end a program, compared on three behaviours. Returning from main and calling exit both run the atexit handlers and flush the streams: the measured file contained all 35 buffered bytes. Calling _Exit runs no handlers and flushes nothing: the measured file contained 0 bytes. Calling abort runs no handlers, and whether streams are flushed is implementation-defined; on this implementation the file did contain all 35 bytes.How it endsatexit handlersStreams flushedMeasured file35 bytes werebufferedreturn from mainrunyes35 bytesexit(status)runyes35 bytes_Exit(status)not runno0 byteseverything lostabort()not runimplementation-defined35 bytes herenot a guarantee12
Figure 1 — the four endings against what each one does on the way out, with the measured result in the last column. The first two rows are the same thing: returning from main is defined as calling exit with that value, so there is no behavioural difference to choose between. The third row is the one to be careful with — _Exit skips the flush, and every byte that had not yet reached the file was simply gone. The fourth row's middle cell is deliberately not a yes or a no: the standard leaves it implementation-defined, and the 35 bytes recorded here are what this machine happened to do rather than something to rely on.

return from main and exit are the same thing. The standard says that returning from main is equivalent to calling exit with that value — and the measurement agrees in every column. There is no behavioural reason to prefer one; the reason to prefer return is stylistic, and §5 is about it.

_Exit discards everything. No handlers, no flush, 0 bytes. This is Chapter 12.4's measurement arriving as a consequence: if the data only reaches the file during the flush, then skipping the flush loses all of it. _Exit exists for the narrow case where that is what you want — a child process that must not flush buffers its parent already owns, or a situation where running cleanup code would make things worse.

abort runs no handlers, and whether streams are flushed is implementation-defined. This implementation flushed them; the figure labels that cell "not a guarantee" because a reader who generalises from one machine will be wrong on another. abort also ends the program abnormally — the status reported is not a value you chose.

5. Which to Use

Return from main when main can reach the decision. Call exit when it cannot.

The distinction is about structure rather than behaviour, since §4 measured them as identical.

Returning from main keeps the exit on the path a reader can follow. Every function returns to its caller, main returns a status, and the program's control flow is one shape all the way out. That is worth something, and it is the default.

exit is for when returning would be worse. A failure five calls deep, where every intermediate function would have to acquire an error-propagation path it otherwise does not need, is the case exit is for. The cost is real and should be acknowledged: exit leaves no opportunity for any caller in between to release a resource, so everything Chapter 9.8 said about ownership stops applying at that point.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void fail(const char *program, const char *message)
    {
        fprintf(stderr, "%s: %s\n", program, message);
        exit(EXIT_FAILURE);
    }

That is a reasonable helper for a small program whose only resources are its streams, which the exit will flush. It is the wrong shape for a library function — a library should return an error, because it does not own the decision to end the caller's program.

atexit registers a function to run on the way out, in reverse order of registration, for exit and for returning from main:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void cleanup(void) { remove(temp_path); }
    atexit(cleanup);

Useful for a genuinely global obligation. Note what it cannot do: it takes no arguments and returns nothing, so anything it needs must be reachable through static storage — which is Chapter 11.5's file-scope object with all the usual caveats.

6. Reporting Failure Usefully

A status is one number, which is not a diagnosis. The division of labour is settled:

The exit status says whether. The message on stderr says what.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (f == NULL) {
        fprintf(stderr, "%s: cannot open %s\n", program, path);
        return EXIT_FAILURE;
    }

Four properties of a message worth having, all visible there:

It goes to stderr, so redirecting the program's output does not hide it — Chapter 12.2 §8.

It names the program, so output from a pipeline of several programs is attributable.

It names the thing that failed, not just the operation. "cannot open" is half a message; "cannot open /etc/config" is a complete one.

It does not end the program by itself. The message and the status are two separate decisions, and keeping them separate is what lets a caller choose to continue.

Chapter 12.8 adds the fifth property — saying why the operation failed, which is errno's job and the next chapter's subject.

7. A Program That Ends Properly

Azvya Education Pvt. Ltd.VLSI Mentor
the whole shape
#include <stdio.h>
#include <stdlib.h>

static const char *program = "tool";

static void usage(void)
{
    fprintf(stderr, "usage: %s PATH\n", program);
}

int main(int argc, char *argv[])
{
    if (argc > 0 && argv[0][0] != '\0') { program = argv[0]; }

    if (argc != 2) {
        usage();
        return EXIT_FAILURE;                       /* a usage error */
    }

    FILE *f = fopen(argv[1], "r");
    if (f == NULL) {
        fprintf(stderr, "%s: cannot open %s\n", program, argv[1]);
        return EXIT_FAILURE;
    }

    int rc = EXIT_SUCCESS;
    char line[256];
    while (fgets(line, sizeof line, f) != NULL) {
        fputs(line, stdout);
    }
    if (ferror(f)) {                               /* 12.4 §3: which failure */
        fprintf(stderr, "%s: error reading %s\n", program, argv[1]);
        rc = EXIT_FAILURE;
    }

    fclose(f);

    if (fflush(stdout) != 0) {                     /* 12.4 §4: the flush can fail */
        fprintf(stderr, "%s: error writing output\n", program);
        rc = EXIT_FAILURE;
    }

    return rc;
}

Five decisions, each from a measurement in this module:

One return from main, with rc accumulating the outcome, so there is a single exit and every path reaches the flush.

EXIT_SUCCESS and EXIT_FAILURE rather than 0 and 1 — §1, and it costs nothing.

ferror after the read loop, because fgets returning NULL means end-of-file or error — Chapter 12.4 §3.

fflush(stdout) checked before returning. This is the one most programs omit. Chapter 12.4 §4 measured that output sits in a buffer; if the program's output is going to a file or a pipe that fails, the failure surfaces at the flush, and a program that returns EXIT_SUCCESS without checking has reported success for output nobody received.

Failure never reached silently. Every path that sets rc to failure also writes a message saying what happened.

8. What You Should Be Able to Say Now

  1. main's return value is the exit status — the program's whole report to its caller.
  2. The standard defines 0, EXIT_SUCCESS and EXIT_FAILURE; everything else is a host convention.
  3. Measured: EXIT_SUCCESS is 0 and EXIT_FAILURE is 1 here — neither value is required.
  4. Measured: only the low eight bits survive. return 256 → 0, return 257 → 1, return -1 → 255.
  5. return 256 reports success. Keep statuses in 0–125, and never compute one from an error code or a count.
  6. Measured: falling off the end of main gives status 0 with no warning — main is the only function where that is defined.
  7. Returning from main is defined as calling exit — measured identical in every column.
  8. Measured: _Exit ran no handlers, flushed nothing, and left 0 of 35 bytes.
  9. Measured: abort ran no handlers; whether it flushes is implementation-defined, and this machine happened to flush.
  10. Return from main when it can reach the decision; use exit when it cannot — and never exit from a library.
  11. atexit runs handlers in reverse order for exit and for returning from main.
  12. The status says whether; stderr says what. Check fflush(stdout) before reporting success.

9. Exercises

Exercise 1 — predict the status. For each, say what the shell sees.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
return 0;      return 1;      return 100;
return 200;    return 256;    return 300;
return -1;     /* no return statement at all */

Exercise 2 — find the lie. A program uses return error_count; as its status. Describe what a build system sees when it processes 256 bad records, and rewrite the ending.

Exercise 3 — choose the ending. For each, say which of the four you would use and why.

  • main finishes its work normally
  • an allocation fails four calls deep in a small command-line tool
  • a child process must terminate without flushing buffers its parent will also flush
  • an internal invariant has been violated and continuing would corrupt data

Exercise 4 — the lost bytes. Explain, using §4's measurement and Chapter 12.4's, why a program that writes a report and then calls _Exit(0) produces an empty file and an exit status of success.

Exercise 5 — write the handler. Register an atexit function that removes a temporary file. Say where the path has to live and why, naming the chapter that explains the constraint.

Exercise 6 — audit the endings. This program has three defects in how it ends. Find them all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int main(int argc, char *argv[]) {
    FILE *f = fopen(argv[1], "r");
    if (!f) { printf("could not open\n"); return 1; }
    int bad = process(f);
    fclose(f);
    return bad;
}

Exercise 7 — the two channels. Explain the division of labour between the exit status and stderr, and say what each of these programs gets wrong: one that prints nothing and returns 3; one that prints a clear error and returns 0.

Exercise 8 — the flush. Write the ending of a program that produces output on stdout and must not report success if that output failed to be written. Say which measurement from Chapter 12.4 makes the check necessary.

10. Summary

  • main's return value is the exit status, the program's entire report.
  • 0, EXIT_SUCCESS, EXIT_FAILURE are the standard's; everything else is local convention.
  • Measured: only eight bits survive — 256 → 0, 257 → 1, −1 → 255. Keep statuses in 0–125.
  • Measured: falling off the end of main is status 0 — the one function where that is defined.
  • return from main is exit — measured identical.
  • Measured: _Exit lost all 35 buffered bytes; abort ran no handlers and its flushing is implementation-defined.
  • Return from main when you can; exit when you cannot; never from a library.
  • The status says whether, stderr says what — and check fflush(stdout) before claiming success.

Every error message in this chapter said what the program was trying to do and stopped there. "Cannot open /etc/config" tells a user which operation failed and leaves them to guess between a missing file, a spelling mistake, a permission problem and a path that is actually a directory — four situations with four different fixes. The library knew which one it was. It recorded the answer in a place this module has touched twice without examining, under a contract with more conditions attached than any other in the standard library: a value that is only meaningful at a particular moment, that nothing ever resets, and that a successful call is free to change. The next chapter is that contract in full, and the three-line pattern that makes it usable.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.