Using fgets and strtol Safely in C
Reading an integer from a terminal appears simple, but user input is never guaranteed to be valid. A dependable C program must distinguish between valid numbers, malformed input, overflow, extra characters, empty lines, and values outside an acceptable range.
One of the safest and most reusable approaches is to separate input into two operations:
- Use
fgetsto read an entire line as text. - Use
strtolto deliberately convert that text into an integer.
This separation gives the program an opportunity to inspect the complete input before accepting it.
Why Input Is More Difficult Than It Looks
Suppose a program asks the user to enter their age. The programmer may expect:
25
The user might instead enter:
-4, which is an integer but may be outside the permitted range25abc, which begins with digits but contains invalid trailing charactershello, which contains no integer- An empty line
- A number too large to fit in the destination type
- A line longer than the input buffer
The computer receives all these inputs as characters. The program must decide whether those characters represent an acceptable value.
Keyboard input begins as text. Conversion should occur only after the program captures and validates that text.
Why Not Rely Only on scanf?
A beginner may initially write:
int age;
printf("Enter your age: ");
scanf("%d", &age);
This can work for valid input, but robust validation and recovery are more complicated. When conversion fails, invalid characters may remain in the input stream and interfere with later reads.
If the user enters:
abc
scanf cannot convert those characters using %d. Unless the program deliberately removes the invalid input, the next operation may encounter the same characters again.
The issue is not that scanf can never be used safely. The issue is that line-oriented input with fgets often makes validation and recovery easier to understand.
The safer sequence is:
- Capture the complete line.
- Convert the stored text.
- Inspect where conversion stopped.
- Reject invalid or out-of-range input.
- Store the result only after every check succeeds.
The fgets Mental Model
fgets reads characters from a stream and stores them in a character array.
fgets(buffer, buffer_size, stdin);
Its three arguments are:
buffer: the destination character arraybuffer_size: the total capacity of that arraystdin: the standard input stream
A basic example is:
#include <stdio.h>
char buffer[100];
if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
fprintf(stderr, "Input could not be read.\n");
}
The return value must always be checked. NULL can represent end-of-file or an input error.
What fgets stores
If the user types 42 and presses Enter, the array usually contains:
'4' '2' '\n' '\0'
'4'and'2'are the entered characters.'\n'represents the Enter key.'\0'terminates the C string.
The newline matters because validation should normally treat it as the end of the user's meaningful input.
Detecting an oversized line
If the user enters more characters than the array can hold, fgets stores only part of the line. The remaining characters stay in stdin.
The program can check whether the captured string contains a newline:
#include <string.h>
if (strchr(buffer, '\n') == NULL) {
/* The complete line may not have fit. */
}
When a line is too long, discard its remainder before requesting another value:
#include <stdio.h>
static void discard_remaining_input(void)
{
int character;
while (
(character = getchar()) != '\n' &&
character != EOF
) {
/* Intentionally discard each character. */
}
}
Using int for the result of getchar is important because it must represent every possible unsigned-character value as well as the special EOF value.
The strtol Mental Model
The name strtol means string to long.
It examines a string and attempts to convert its initial numeric portion into a value of type long.
char *end = NULL;
long value = strtol(buffer, &end, 10);
The arguments mean:
buffer: the text to convert&end: wherestrtolstores the stopping position10: use decimal notation
The end pointer is essential because it identifies the first character that was not part of the conversion.
For example:
- Input
"42\n"produces42, withendpointing at the newline. - Input
"42abc\n"produces42, withendpointing at'a'. - Input
"hello\n"performs no conversion, withendequal tobuffer. - Input
"-15\n"produces-15, withendpointing at the newline.
A returned value of zero is not sufficient evidence of success. Both valid input such as "0" and invalid input such as "hello" may result in a numeric value of zero. The end pointer determines whether any digits were converted.
Validate Every Conversion Deliberately
A dependable conversion should perform its checks in a predictable order.
Confirm that input was read
if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
/* End-of-file or an input error occurred. */
}
Confirm that the complete line was captured
if (strchr(buffer, '\n') == NULL) {
discard_remaining_input();
/* Reject the oversized line. */
}
Clear errno before conversion
#include <errno.h>
errno = 0;
value = strtol(buffer, &end, 10);
This ensures that a later ERANGE check describes the current conversion rather than an earlier library operation.
Confirm that digits were converted
if (end == buffer) {
/* The input did not begin with an integer. */
}
Detect overflow or underflow
if (errno == ERANGE) {
/* The value cannot be represented as long. */
}
Reject invalid trailing characters
Spaces and tabs may be permitted after the integer, but other characters should be rejected.
while (*end == ' ' || *end == '\t') {
end++;
}
if (*end != '\n' && *end != '\0') {
/* Unexpected trailing characters remain. */
}
This accepts 42 followed by spaces, while rejecting 42abc.
Confirm that the value fits in int
strtol returns long. If the destination is int, validate its range before casting.
#include <limits.h>
if (value < INT_MIN || value > INT_MAX) {
/* The value cannot be represented as int. */
}
Do not cast before checking. A premature conversion can produce a result that no longer represents the entered value.
Build a Reusable read_int Function
Instead of repeating validation throughout a program, place it behind a clear interface. A Boolean success/failure result is tempting, but it hides a distinction the caller often needs: malformed text can be retried, while end-of-file and stream errors should stop the loop.
Use an explicit status type:
#ifndef READ_INT_H
#define READ_INT_H
enum read_int_status {
READ_INT_OK,
READ_INT_INVALID,
READ_INT_END,
READ_INT_IO_ERROR
};
enum read_int_status read_int(
const char *prompt,
int *result
);
#endif
This interface communicates four outcomes:
READ_INT_OK: a complete, validintwas storedREAD_INT_INVALID: input was received but was malformed, oversized, or out of rangeREAD_INT_END: the stream reached end-of-file before supplying another valueREAD_INT_IO_ERROR: the stream itself failed
The implementation prints an optional prompt, captures one logical line, validates the complete conversion, and modifies the output only on success.
#include "read_int.h"
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INPUT_BUFFER_SIZE 128
static void discard_remaining_input(void)
{
int character;
while (
(character = getchar()) != '\n' &&
character != EOF
) {
/* Intentionally discard each character. */
}
}
enum read_int_status read_int(
const char *prompt,
int *result
)
{
char buffer[INPUT_BUFFER_SIZE];
char *end = NULL;
long value;
if (result == NULL) {
return READ_INT_INVALID;
}
if (prompt != NULL) {
printf("%s", prompt);
fflush(stdout);
}
if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
if (feof(stdin)) {
return READ_INT_END;
}
return READ_INT_IO_ERROR;
}
/* A final line may legitimately end at EOF without a newline. */
if (strchr(buffer, '\n') == NULL && !feof(stdin)) {
discard_remaining_input();
return READ_INT_INVALID;
}
errno = 0;
value = strtol(buffer, &end, 10);
if (end == buffer) {
return READ_INT_INVALID;
}
if (errno == ERANGE) {
return READ_INT_INVALID;
}
while (*end != '\0' && isspace((unsigned char)*end)) {
end++;
}
if (*end != '\0') {
return READ_INT_INVALID;
}
if (value < INT_MIN || value > INT_MAX) {
return READ_INT_INVALID;
}
*result = (int)value;
return READ_INT_OK;
}
The cast passed to isspace is deliberate. Character-classification functions require either EOF or a value representable as unsigned char; passing a negative plain char can invoke undefined behavior.
The output variable is modified only after every validation check succeeds. Failed input therefore cannot partially change the caller's state. The !feof(stdin) condition also avoids incorrectly rejecting a complete final line that ends at EOF without a newline.
Use the Function From a Program
#include "read_int.h"
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int number;
enum read_int_status status;
status = read_int("Enter an integer: ", &number);
switch (status) {
case READ_INT_OK:
printf("You entered: %d\n", number);
return EXIT_SUCCESS;
case READ_INT_INVALID:
fprintf(stderr, "Invalid integer input.\n");
return EXIT_FAILURE;
case READ_INT_END:
fprintf(stderr, "No input was available.\n");
return EXIT_FAILURE;
case READ_INT_IO_ERROR:
perror("stdin");
return EXIT_FAILURE;
}
return EXIT_FAILURE;
}
Compile both translation units:
clang \
-std=c17 \
-Wall \
-Wextra \
-Wpedantic \
-Wconversion \
-Wshadow \
-Wformat=2 \
-Werror \
main.c \
read_int.c \
-o safe_input
Run the program:
./safe_input
An invalid and valid interaction might look like this:
$ ./safe_input
Enter an integer: hello
Invalid integer input.
$ ./safe_input
Enter an integer: 24
You entered: 24
Add Application-Specific Range Validation
An integer can be syntactically valid but still unacceptable to a particular operation.
If a program asks how many numbers the user wants to enter, it might accept only values from 1 through 100.
#include "read_int.h"
#include <stdio.h>
enum read_int_status read_int_in_range(
const char *prompt,
int minimum,
int maximum,
int *result
)
{
int value;
enum read_int_status status;
if (result == NULL || minimum > maximum) {
return READ_INT_INVALID;
}
for (;;) {
status = read_int(prompt, &value);
if (status == READ_INT_END ||
status == READ_INT_IO_ERROR) {
return status;
}
if (status == READ_INT_INVALID) {
printf("Please enter a valid integer.\n");
continue;
}
if (value < minimum || value > maximum) {
printf(
"Enter a value from %d through %d.\n",
minimum,
maximum
);
continue;
}
*result = value;
return READ_INT_OK;
}
}
Example usage:
int count;
enum read_int_status status;
status = read_int_in_range(
"How many numbers? ",
1,
100,
&count
);
if (status != READ_INT_OK) {
/* EOF and stream errors stop the operation. */
}
This separation gives the functions distinct responsibilities:
read_intdecides whether the text represents anintand reports why reading stopped.read_int_in_rangedecides whether that valid integer is acceptable to the application.
Most importantly, read_int_in_range does not retry forever after end-of-file. Recoverable validation failures and terminal stream conditions follow different control paths.
Test More Than the Successful Path
An input function proves its value through failure cases.
Test at least:
42: valid positive integer-17: valid negative integer when permitted42followed by spaces: valid when trailing whitespace is allowed12abc: invalid trailing charactershello: no conversion999999999999999999999: likely outside the range oflong- An empty line: no integer
- A line larger than the input buffer: must be rejected and discarded
2147483648on a system with 32-bitint: outside the range ofint- End-of-file: the read operation cannot provide a new value
- A valid final line without a newline before end-of-file
- A simulated stream error when the test environment supports one
Testing should also verify that an invalid attempt does not change the caller's existing output value.
Common Mistakes to Avoid
Ignoring the result of fgets
fgets(buffer, sizeof(buffer), stdin);
/* The program incorrectly assumes input was read. */
Always test for NULL.
Discarding the end pointer
value = strtol(buffer, NULL, 10);
Passing NULL prevents the program from determining whether trailing characters remained.
Forgetting to clear errno
Set errno = 0 immediately before strtol. Otherwise, an earlier operation may have left an unrelated error value behind.
Casting before validating
int number = (int)strtol(
buffer,
&end,
10
);
Store the result in long, validate it, compare it with INT_MIN and INT_MAX, and only then cast.
Leaving an oversized line in stdin
If the complete line does not fit, the next fgets may read leftover characters instead of new input. Discard the remainder before continuing.
Modifying output before validation finishes
Do not write to the caller's result until every check succeeds. This preserves a clear failure invariant.
The Safe-Input Checklist
When combining fgets and strtol, verify that:
- The destination buffer has a known capacity.
- The result of
fgetsis checked. - Oversized lines are detected and discarded.
errnois cleared before conversion.- The end pointer is compared with the buffer.
ERANGEis checked.- Unexpected trailing characters are rejected.
- The
longresult is checked against the destination type's range. - The caller's output is stored only after complete validation.
- Invalid input, end-of-file, and stream errors have distinguishable results.
- Valid, invalid, boundary, oversized, and end-of-file cases are tested.
Input Validation Is Interface Design
Safe input handling is not only about choosing a library function. It is about designing a boundary between untrusted text and trusted program state.
fgets captures the input record. strtol converts deliberately and reports where conversion stopped. Range checks protect the destination type. Explicit status values preserve the difference between retryable input and a stream that has ended or failed. Output parameters remain unchanged until validation succeeds. Tests exercise both ordinary and hostile input.
This same discipline applies beyond interactive programs. Configuration parsers, command-line tools, network services, file readers, compilers, and runtime systems all receive data that must be validated before it becomes internal state.
The strongest implementation is therefore not the one that accepts 42. It is the one that can explain—and correctly handle—every important way the input might not be 42.