C course menu
Error Handling in C: Return Codes, errno, perror and assert
Handle errors in C, which has no exceptions: checking return values, error codes and out-parameters, errno with perror and strerror, cleaning up with goto, reporting errors to stderr, exit and EXIT_FAILURE, and assert for programming mistakes.
On this page
C has no exceptions. Functions report failure through their return value, and the caller must check it. Ignoring return values is one of the most common sources of bugs in C programs.
Checking return values#
Standard library functions signal failure in documented ways:
Look up each function's failure value, and check it.
Designing your own functions#
A common style returns a status code and passes results back through pointers:
#include <stdio.h>
typedef enum { OK = 0, ERR_DIVIDE_BY_ZERO, ERR_OUT_OF_RANGE } Status;
const char *status_text(Status s) {
switch (s) {
case OK: return "ok";
case ERR_DIVIDE_BY_ZERO: return "division by zero";
case ERR_OUT_OF_RANGE: return "result out of range";
}
return "unknown error";
}
Status percentage(int part, int whole, int *out) {
if (whole == 0) return ERR_DIVIDE_BY_ZERO;
int p = part * 100 / whole;
if (p < 0 || p > 100) return ERR_OUT_OF_RANGE;
*out = p;
return OK;
}
int main(void) {
int tests[][2] = {{45, 60}, {5, 0}, {80, 40}};
for (int i = 0; i < 3; i++) {
int result;
Status s = percentage(tests[i][0], tests[i][1], &result);
if (s == OK) printf("%d/%d = %d%%\n", tests[i][0], tests[i][1], result);
else printf("%d/%d: error: %s\n", tests[i][0], tests[i][1], status_text(s));
}
return 0;
}45/60 = 75%
5/0: error: division by zero
80/40: error: result out of rangeOther conventions: return -1 for failure and 0 or a count for success (common in POSIX functions), or return NULL for a failed pointer result. Whatever you choose, use it consistently and document it.
errno, perror and strerror#
Many library functions also set the global variable errno (from <errno.h>) to a code describing why they failed. perror prints your message followed by the description; strerror(errno) returns the description as a string:
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void) {
FILE *f = fopen("no-such-file.txt", "r");
if (f == NULL) {
int err = errno; /* save it: later calls may change errno */
printf("errno %d (%s)\n", err, err == ENOENT ? "ENOENT" : "other");
printf("strerror: %s\n", strerror(err));
errno = err;
perror("no-such-file.txt"); /* writes to stderr */
return 0;
}
fclose(f);
return 0;
}errno 2 (ENOENT)
strerror: No such file or directory
no-such-file.txt: No such file or directory- Check
errnoonly after a function has reported failure; successful calls may leave any value in it. - Save
errnoin a variable straight away if you will call other functions before using it. strtolsetserrnotoERANGEwhen a number is too large, so seterrno = 0before calling it and check afterwards.- The message text can differ between systems; the constants such as
ENOENTare portable.
Cleaning up on errors#
When a function acquires several resources, each failure must release what was already acquired. A goto to cleanup labels in reverse order keeps this tidy:
#include <stdio.h>
#include <stdlib.h>
int copy_upper(const char *src_path, const char *dst_path) {
int result = -1;
FILE *in = NULL, *out = NULL;
char *buf = NULL;
in = fopen(src_path, "r");
if (in == NULL) goto done;
out = fopen(dst_path, "w");
if (out == NULL) goto done;
buf = malloc(256);
if (buf == NULL) goto done;
while (fgets(buf, 256, in) != NULL) {
for (char *p = buf; *p; p++) if (*p >= 'a' && *p <= 'z') *p -= 32;
fputs(buf, out);
}
result = 0;
done:
free(buf); /* free(NULL) is safe */
if (out) fclose(out);
if (in) fclose(in);
return result;
}
int main(void) {
FILE *f = fopen("in.txt", "w");
if (f == NULL) return 1;
fputs("hello\nerrors\n", f);
fclose(f);
printf("first: %d\n", copy_upper("in.txt", "out.txt"));
printf("second: %d\n", copy_upper("missing.txt", "out2.txt"));
char line[32];
f = fopen("out.txt", "r");
if (f == NULL) return 1;
while (fgets(line, sizeof line, f)) printf("%s", line);
fclose(f);
return 0;
}first: 0
second: -1
HELLO
ERRORSWhatever fails, every resource acquired so far is released exactly once, and the function returns -1 to its caller.
exit#
exit(status) (from <stdlib.h>) ends the program immediately from anywhere, flushing and closing open FILEs. Use EXIT_SUCCESS and EXIT_FAILURE. It is fine for small tools; library code should return errors to the caller instead and let it decide.
assert: catching programming mistakes#
assert(condition) from <assert.h> stops the program with a message if the condition is false. Use it for things that can only be false because of a bug, not for user input or failures such as a missing file:
#include <assert.h>
double average(const int *a, int n) {
assert(a != NULL && n > 0); /* callers must never pass these */
/* ... */
}When it fails, it prints the expression, the file and the line, then aborts. Compiling with -DNDEBUG removes all asserts, so never put necessary work inside one.
Practice#
Practice exercise
Write int parse_port(const char *text, int *port) that returns 0 on success and -1 on failure. It should reject empty strings, extra characters, and values outside 1 to 65535, and use errno to catch numbers too large for a long. Test it with "8080", "0", "80a" and "99999999999999999999".
Show solution
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int parse_port(const char *text, int *port) {
char *end;
errno = 0;
long v = strtol(text, &end, 10);
if (end == text || *end != '\0') return -1; /* empty or extra text */
if (errno == ERANGE) return -1; /* too large for long */
if (v < 1 || v > 65535) return -1;
*port = (int)v;
return 0;
}
int main(void) {
const char *inputs[] = {"8080", "0", "80a", "99999999999999999999"};
for (int i = 0; i < 4; i++) {
int port;
if (parse_port(inputs[i], &port) == 0) printf("%s -> port %d\n", inputs[i], port);
else printf("%s -> invalid\n", inputs[i]);
}
return 0;
}8080 -> port 8080
0 -> invalid
80a -> invalid
99999999999999999999 -> invalidKey takeaway#
Finished this lesson?