C / FILES AND ERRORS
Return-value checking as a habit, not an afterthought
Check the result of every C call that can fail, matching each function's own failure signal, so errors surface at the call site instead of later damage.
What you will learn
- Name the failure signal of each call: NULL, negative, nonzero, or a short count
- Write the if before the success path so no call escapes unchecked
- End a counting loop with ferror to separate clean end-of-input from a real error
- Make every failed check alter control flow: return, goto cleanup, or exit
Understanding Return-value checking as a habit, not an afterthought
C has no exceptions, so a function that fails has exactly one in-band way to tell you: the value it hands back. If you discard that value, the failure does not go anywhere else; it simply becomes a wrong assumption that the next twenty lines are built on. A useful reframe is that the return value is not a bonus you may collect but the second half of the call: fopen returns a stream and a verdict, fwrite returns a progress report, fseek returns a verdict only. The bug you get from skipping the verdict never points at the call that failed, which is why these bugs cost so much to find.
The awkward part is that each function encodes failure differently, because one return type has to carry both the useful result and the status. Pointer-returning functions like fopen, fgets and malloc use NULL. Output functions like printf and fprintf return a character count and go negative on error. Verdict-only functions like fclose, fseek, remove and rename return 0 for success and nonzero for failure, which is the opposite of the usual truthiness reflex. fread and fwrite return an unsigned count, so failure looks like a number smaller than what you asked for, never a negative one; fscanf returns how many items it assigned; snprintf returns the length it wanted, so truncation shows up as a value greater than or equal to the buffer size. There is no single test that covers all of these, so the habit is per-function, not generic.
Where the check goes matters as much as whether it exists. Test right at the call, and the unreliable value becomes a checked invariant: after `if (f == NULL) return -1;`, the remaining code can treat f as a real stream and stay straight-line, with no repeated defensive tests. A check must also change control flow. An if that prints a warning and then falls through into the same code path is worse than no check at all, because the program now looks careful while still dereferencing a null stream three lines later. When several calls must succeed in sequence and each success owns a resource, funnel the failures into one cleanup label so the early successes are released exactly once.
<stdio.h>
/* Each call is followed immediately by a test against that
function's own failure value. */
static int save_scores(const char *path, const int *scores, size_t n)
{
FILE *f = fopen(path, "w");
if (f == NULL) /* fopen reports failure as NULL */
return -1;
for (size_t i = 0; i < n; i++) {
if (fprintf(f, "%d\n", scores[i]) < 0) { /* fprintf: negative */
fclose(f);
return -1;
}
}
if (fclose(f) != 0) /* fclose: nonzero, so 0 is the good case */
return -1;
return 0;
}
int main(void)
{
int scores[] = { 12, 7, 40 };
if (save_scores("scores.txt", scores, 3) != 0) {
printf("could not save scores\n");
return 1;
}
printf("saved 3 scores\n");
FILE *f = fopen("scores.txt", "r");
if (f == NULL) {
printf("could not reopen scores.txt\n");
return 1;
}
int value, total = 0, count = 0;
while (fscanf(f, "%d", &value) == 1) { /* fscanf: items assigned */
total += value;
count++;
}
if (ferror(f)) /* the loop stopped: EOF or error? */
printf("read error after %d values\n", count);
fclose(f);
printf("read %d values, total %d\n", count, total);
FILE *bad = fopen("no-such-dir/scores.txt", "r");
if (bad == NULL)
printf("fopen on a bad path returned NULL, as expected\n");
else
fclose(bad);
return 0;
}
Every C function that can fail reports it through its return value in its own dialect, so the value has to be tested at the call site or the failure vanishes.
Worked examples
Three functions, three failure dialects
The same idea of checking the result looks completely different for snprintf, strtol and remove.
<stdio.h>
<stdlib.h>
int main(void)
{
char buf[8];
int id = 1234567;
/* snprintf returns the length it WANTED, so >= size means truncation. */
int need = snprintf(buf, sizeof buf, "user-%d", id);
if (need < 0)
printf("snprintf: encoding error\n");
else if ((size_t)need >= sizeof buf)
printf("snprintf: truncated, needed %d bytes, buf holds %zu\n",
need, sizeof buf);
/* strtol hides "how far did I get" in the end pointer, not in the value. */
const char *text = "12abc";
char *end;
long n = strtol(text, &end, 10);
if (end == text)
printf("strtol: no digits at all\n");
else if (*end != '\0')
printf("strtol: parsed %ld but stopped at \"%s\"\n", n, end);
/* remove returns 0 on success, so a plain truth test would be inverted. */
if (remove("definitely-not-here.txt") != 0)
printf("remove: failed, as expected\n");
return 0;
}
Example explained
Line 1snprintf wrote "user-12" plus a terminator but returned 12, the length of the full "user-1234567", which is how truncation is detected.
Line 2The cast to size_t in `(size_t)need >= sizeof buf` avoids comparing a signed int with an unsigned sizeof result; the separate `need < 0` test still catches real encoding errors.
Line 3strtol returned 12 and set end to "abc"; the return value alone cannot distinguish "12abc" from "12", so the end pointer is the check.
Line 4remove reports success as 0, so `if (remove(path))` reads as "if it failed" and `if (!remove(path))` means "if it worked".
One cleanup path for a chain of calls
When each successful call owns a resource, failed checks jump to a single exit that releases only what was acquired.
<stdio.h>
<stdlib.h>
int main(void)
{
int status = 1;
char *buf = malloc(64);
FILE *f = NULL;
if (buf == NULL) {
printf("out of memory\n");
goto done;
}
f = fopen("no-such-dir/report.txt", "w");
if (f == NULL) {
printf("cannot open report.txt\n");
goto done;
}
if (fprintf(f, "ok\n") < 0) {
printf("write failed\n");
goto done;
}
if (fclose(f) != 0) {
f = NULL;
printf("close failed\n");
goto done;
}
f = NULL;
status = 0;
done:
if (f != NULL)
fclose(f);
free(buf);
printf("exit status %d\n", status);
return status;
}
Example explained
Line 1status starts at 1 so every escape route is a failure by default, and only the line after the last successful check sets it to 0.
Line 2The malloc check passed, so the fopen failure must still free buf; the shared `done` label makes that automatic instead of duplicated.
Line 3f is set to NULL both after a successful fclose and after a failed one, because closing an already-closed stream a second time is undefined behaviour.
Line 4free(NULL) is legal, which is why the cleanup block only needs a guard for the stream, not for the pointer.
Important notes
Ignoring a result is a decision, not a default. On a stream you only read from, the fclose value carries no risk of lost data, which is why you often see it dropped; on a stream you wrote to, dropping it throws away the only report you get about the final flush.
Compilers help if you let them: gcc and clang with -Wall warn when you discard the result of functions declared warn_unused_result, and writing `(void)printf(...)` documents that the omission was intentional rather than forgotten.
Common mistakes
Testing an unsigned count for negativity: `if (fwrite(data, sizeof(int), 100, f) < 0)` can never be true because the return type is size_t, so a short write on a full disk is silently accepted and the file ends up truncated.
Inverting the verdict functions, as in `if (!remove(path)) printf("could not delete");` - remove returns 0 on success, so the program announces failure exactly when it worked and stays quiet when it did not.
Checking a stream pointer but continuing anyway: printing "warning: could not open file" and then calling fgets on the NULL result. The program crashes one line below the check that was supposed to prevent it, and the message makes the log look handled.
Try it yourself
Change, predict, then run
Write a copy_first_line(src, dst) function that checks fopen twice, fgets, fputs and both fclose calls, returning a different nonzero code for each failure. Call it once with a real file and once with a missing source, and confirm the missing-source run prints one message and never creates the destination file.
Open the C workspaceCheck your understanding
A program writes an array with `n = fwrite(data, sizeof(int), 100, f);` and guards it with `if (n < 0) { report_error(); }`. Why can this guard never fire, even when the write is incomplete?
- fwrite returns size_t, an unsigned type, so a partial result such as 37 is not negative and the comparison is always false
- fwrite returns a byte count, which is positive whenever sizeof(int) is at least 1
- fwrite cannot fail on its own; only the later fclose is able to report a write error
- The optimiser removes the comparison because fwrite is declared with warn_unused_result
Show answer
fwrite's return type is size_t, so it reports trouble by returning fewer items than requested, and an unsigned value is never below zero; the only working test is `if (n != 100)`. Option three is tempting because buffered data really can fail later at flush time, but fwrite itself does return short counts when the stream is in an error state or the device is full, and that has to be caught at the call.