C / FILES AND ERRORS
Writing a program that refuses bad input gracefully
Validate and reject malformed input in C with strtol's endptr and ERANGE, report a precise reason on stderr, and exit with a telling status.
What you will learn
- Use strtol's endptr to tell 'no digits at all' apart from 'digits then junk'
- Set errno = 0 before strtol so an ERANGE afterwards really came from this call
- Drain the tail of an over-long line so the next fgets starts on a real record
- Refuse on stderr, leave the destination unchanged, and exit with a distinct code
Understanding Writing a program that refuses bad input gracefully
A program that reads input has two respectable outcomes: it accepted the input and did the work, or it refused the input and did nothing. The refusal path deserves as much design as the success path, because a half-refusal is worse than a crash: it leaves a record partly applied and a message that does not say which field was wrong. Treat validation as a conversion from untrusted bytes into either a value you can reason about or a specific reason you cannot, with no third option.
This is why atoi and a bare scanf("%d") cannot refuse anything: atoi("12kg") returns 12, atoi("abc") returns 0, and neither answer can be distinguished from a real number. strtol reports two things the return value alone cannot: where it stopped, through endptr, and whether the digits overflowed, through errno == ERANGE. If endptr still points at the start of the text, not one digit was consumed; if it points at something other than the terminator, you got digits followed by junk. Set errno = 0 before the call, because the library only ever sets errno and never clears it, so a stale ERANGE from an earlier call would make a perfectly good number look too large.
The other half of graceful refusal is state. Your data is state, so nothing should be written to the destination on a path that ends in a refusal; the file position is state too, so if you reject an over-long line without discarding its tail, the next fgets reads that tail as a fresh record and one bad line poisons everything after it. Send the message to stderr so it still reaches a human when stdout is redirected into a file, name the record and the offending text, and return a nonzero status distinct from a crash so a caller can tell "your data is bad" from "I fell over".
<stdio.h>
<stdlib.h>
<errno.h>
enum parse_result { PARSE_OK, PARSE_EMPTY, PARSE_NOT_A_NUMBER, PARSE_TRAILING, PARSE_RANGE };
static int is_blank(const char *s)
{
while (*s == ' ' || *s == '\t' || *s == '\n')
s++;
return *s == '\0';
}
/* Turn one line of text into a long, or say exactly why it cannot be one. */
static enum parse_result parse_count(const char *text, long *out)
{
char *end;
long value;
errno = 0; /* strtol only sets errno, it never clears it */
value = strtol(text, &end, 10);
if (end == text) /* not a single digit was consumed */
return is_blank(text) ? PARSE_EMPTY : PARSE_NOT_A_NUMBER;
if (errno == ERANGE)
return PARSE_RANGE;
while (*end == ' ' || *end == '\t' || *end == '\n')
end++;
if (*end != '\0') /* digits, then something else */
return PARSE_TRAILING;
*out = value; /* written only on success */
return PARSE_OK;
}
static const char *reason(enum parse_result r)
{
switch (r) {
case PARSE_EMPTY: return "no value on this line";
case PARSE_NOT_A_NUMBER: return "not a number";
case PARSE_TRAILING: return "unexpected text after the number";
case PARSE_RANGE: return "value too large for a long";
default: return "unknown";
}
}
int main(void)
{
static const char *const lines[] = {
"42\n", " 7 \n", "\n", "twelve\n", "12kg\n", "99999999999999999999\n"
};
const size_t count = sizeof lines / sizeof lines[0];
size_t i, accepted = 0;
long total = 0;
for (i = 0; i < count; i++) {
long value;
enum parse_result r = parse_count(lines[i], &value);
if (r == PARSE_OK) {
total += value;
accepted++;
} else {
fprintf(stderr, "line %zu refused: %s\n", i + 1, reason(r));
}
}
printf("accepted %zu of %zu lines, total %ld\n", accepted, count, total);
return accepted == count ? EXIT_SUCCESS : 2;
}
Refusing input is a designed code path: classify exactly what is wrong, report it precisely, and leave both your data and your stream position in a known-good state.
Worked examples
Refusing a line without losing your place
An over-long record is rejected, and the rest of it is discarded so the following record still parses.
<stdio.h>
<string.h>
/* Throw away whatever is left of a line that did not fit in the buffer. */
static void drop_rest_of_line(FILE *f)
{
int c;
while ((c = getc(f)) != '\n' && c != EOF)
;
}
int main(void)
{
FILE *f = tmpfile();
char line[8];
char *nl;
int n = 0;
if (f == NULL) {
fprintf(stderr, "cannot create temporary file\n");
return 1;
}
fputs("ok\nthis-line-is-far-too-long\nfine\n", f);
rewind(f);
while (fgets(line, sizeof line, f) != NULL) {
n++;
nl = strchr(line, '\n');
if (nl == NULL) {
printf("record %d refused: longer than %d characters\n",
n, (int)(sizeof line - 1));
drop_rest_of_line(f);
} else {
*nl = '\0';
printf("record %d accepted: \"%s\"\n", n, line);
}
}
fclose(f);
return 0;
}
Example explained
Line 1fgets stops when the buffer is full and does not treat that as an error, so a missing '\n' is the only evidence the record was cut short.
Line 2strchr(line, '\n') is a safer test than indexing line[strlen(line) - 1], which would read out of bounds if the string were empty.
Line 3drop_rest_of_line consumes bytes up to and including the newline, so the next fgets begins at a genuine record boundary.
Line 4Because the position was repaired, refusing record 2 costs nothing: record 3 is read as "fine" rather than as "o-long".
Validate everything before changing anything
A good first field is thrown away with the bad second field, so the destination struct is never left half-updated.
<stdio.h>
<stdlib.h>
struct settings { int width; int height; };
static int read_field(const char *text, int lo, int hi, int *out)
{
char *end;
long v = strtol(text, &end, 10);
if (end == text || *end != '\0' || v < lo || v > hi)
return 0;
*out = (int)v;
return 1;
}
/* Nothing in *s changes unless every field passes. */
static int load_settings(const char *w, const char *h, struct settings *s, const char **bad)
{
struct settings tmp;
if (!read_field(w, 1, 4096, &tmp.width)) { *bad = "width"; return 0; }
if (!read_field(h, 1, 4096, &tmp.height)) { *bad = "height"; return 0; }
*s = tmp;
return 1;
}
int main(void)
{
struct settings s = { 640, 480 };
const char *bad = NULL;
if (load_settings("800", "-3", &s, &bad))
printf("applied %dx%d\n", s.width, s.height);
else
printf("refused: bad %s value; still using %dx%d\n", bad, s.width, s.height);
return 0;
}
Example explained
Line 1end == text catches an empty or all-letters field, *end != '\0' catches trailing junk like 800px, and the lo/hi test catches -3, which strtol itself parses quite happily.
Line 2read_field writes to *out only after every test passes, so a caller can never pick up a value from a failed field.
Line 3tmp holds the candidates, so the valid width of 800 is discarded together with the bad height instead of being half-applied.
Line 4s still contains 640 and 480 after the refusal, which is why main can report the problem and keep running on a known-good configuration.
Important notes
strtol quietly accepts leading whitespace and a leading + or -, so " +5" parses as 5; if your format forbids that, you must inspect the text yourself because strtol will not complain.
Diagnostics belong on stderr, but the two streams are buffered separately, so interleaved lines can appear out of order when both are captured into one file; the main example sidesteps this by doing all its stdout output at the end.
Common mistakes
Validating with atoi: atoi("12kg") returns 12 and atoi("abc") returns 0, so malformed records are processed as ordinary numbers and the bug only surfaces later as a wrong total.
Printing a refusal and then using the output variable anyway; on that path it was never assigned, so the program stores or prints an indeterminate value that looks entirely plausible.
Rejecting an over-long line and looping immediately, leaving its tail in the stream, so the leftover fragment becomes the next "record" and everything after it is misread.
Try it yourself
Change, predict, then run
Add a rule to parse_count that refuses any value outside 0 to 1000 with its own reason string, then run it on "-4\n", "1001\n" and "1000\n" and confirm that only the last is accepted.
Open the C workspaceCheck your understanding
A retry loop reads with: if (scanf("%d", &n) != 1) { printf("bad input, try again\n"); continue; }. The user types abc. What happens?
- n is set to 0 and the loop continues using that value
- The message prints once, then the next scanf reads the following line
- The message prints over and over, because the unconvertible characters stay in the stream
- scanf returns EOF, so the loop ends quietly
Show answer
scanf stops at the first character that cannot match %d and leaves it, plus everything after it, unread; the next iteration meets the same 'a' and fails again, forever. Option 1 is tempting because fgets does consume a whole line, but scanf is not line-oriented: it removes only what it managed to match, so refusing input here also means draining the offending characters (for example with getchar until '\n') before retrying.