C / FUNCTIONS
Return values, void and signalling failure
Return single values from C functions, use void and bare return correctly, and signal failure with sentinels, status codes or out-parameters.
What you will learn
- return both hands one value back and ends the function on the spot
- Use a bare return; to leave a void function early; never return a value from it
- Reserve sentinels for values the function can never produce, like -1 or NULL
- Return a status and pass a pointer when every value in range is a valid result
Understanding Return values, void and signalling failure
A return statement does two jobs at once: it converts its expression to the function's declared return type, and it ends the function immediately, so nothing after it in that branch runs. The conversion is silent, which is why returning 3.9 from a function declared int hands back 3, and why returning -1 from a function declared unsigned int hands back a huge positive number. What the caller receives is a copy of that value, and it is an expression rather than a variable: you can store it, test it, pass it on, or ignore it, but you cannot assign to it or take its address.
A return type of void means the value slot is empty. Inside such a function a bare return with no expression is legal and is the normal way to leave early, while returning a value from it is rejected by the compiler. The mirror-image mistake compiles happily: a non-void function that reaches its closing brace without returning anything. The behaviour is undefined the moment the caller uses that result, so what you observe depends on the optimiser and on whatever happened to be in the return register.
C has no exceptions, so failure has to travel inside the same one-value channel or beside it. Two designs cover almost everything. An in-band sentinel is a value the function could never legitimately produce: -1 from something that otherwise returns an index, NULL from malloc and fopen, or EOF from getchar, which is exactly why getchar returns int rather than char, since no unsigned char value can equal EOF. An out-of-band status makes the return value a pure verdict and sends the payload through a pointer argument or a returned struct; errno is a third channel that only becomes meaningful after a function has already reported failure.
Reach for a sentinel only when you can prove the value is impossible. If zero, or -1, or any other marker you pick is a result the function might really compute, the caller receives an answer it cannot interpret.
<stdio.h>
<stdlib.h>
<errno.h>
<limits.h>
/* Returns 1 and writes the number to *out on success.
Returns 0 on failure and leaves *out untouched. */
int parse_int(const char *text, int *out)
{
char *end;
long v;
errno = 0;
v = strtol(text, &end, 10);
if (end == text || *end != '\0') /* no digits at all, or trailing junk */
return 0;
if (errno == ERANGE || v < INT_MIN || v > INT_MAX)
return 0;
*out = (int)v;
return 1;
}
void show(const char *text, int ok, int value)
{
if (!ok) {
printf("[%s] -> rejected\n", text);
return; /* void function: return with no value */
}
printf("[%s] -> %d\n", text, value);
}
int main(void)
{
const char *inputs[] = { "42", "-7", "12x", "", "99999999999999" };
size_t i;
for (i = 0; i < sizeof inputs / sizeof inputs[0]; i++) {
int value = 0;
int ok = parse_int(inputs[i], &value);
show(inputs[i], ok, value);
}
return 0;
}
A function hands back exactly one value, so signalling failure means either reserving a value the function can never produce or splitting the answer from the verdict.
Worked examples
Sentinel versus status: why 0 cannot mean both
The same digit-summing function written with an in-band failure value and then with a separate verdict, showing what the caller can and cannot distinguish.
<stdio.h>
/* In-band failure: 0 means "not a number" and also "the digits summed to 0". */
int sum_digits_bad(const char *s)
{
int total = 0;
if (*s == '\0')
return 0;
for (; *s != '\0'; s++) {
if (*s < '0' || *s > '9')
return 0;
total += *s - '0';
}
return total;
}
/* Out-of-band failure: the verdict is returned, the number goes to *out. */
int sum_digits(const char *s, int *out)
{
int total = 0;
if (*s == '\0')
return 0;
for (; *s != '\0'; s++) {
if (*s < '0' || *s > '9')
return 0;
total += *s - '0';
}
*out = total;
return 1;
}
int main(void)
{
int value, ok;
printf("bad('0') = %d\n", sum_digits_bad("0"));
printf("bad('x') = %d\n", sum_digits_bad("x"));
value = -1;
ok = sum_digits("0", &value);
printf("ok('0') = %d, value = %d\n", ok, value);
value = -1;
ok = sum_digits("x", &value);
printf("ok('x') = %d, value = %d\n", ok, value);
return 0;
}
Example explained
Line 1The first two lines are identical because 0 is both a real sum and the failure marker, so the caller has no way to tell them apart.
Line 2sum_digits returns 1 or 0 as the verdict, so success with a sum of 0 and outright rejection now look different.
Line 3value stays -1 after the failing call because sum_digits writes through out only on the success path.
Line 4ok is stored in its own variable before the printf: reading value in the same argument list as the call that fills it would depend on unspecified evaluation order.
Bundling value and verdict in a returned struct
Returning a small struct carries both the result and the success flag in one value, with no out-parameter.
<stdio.h>
struct result {
int ok;
double value;
};
struct result safe_div(double a, double b)
{
struct result r;
if (b == 0.0) {
r.ok = 0;
r.value = 0.0;
return r;
}
r.ok = 1;
r.value = a / b;
return r;
}
void print_result(const char *label, struct result r)
{
if (!r.ok) {
printf("%s = undefined\n", label);
return;
}
printf("%s = %.2f\n", label, r.value);
}
int main(void)
{
print_result("7/2", safe_div(7.0, 2.0));
print_result("7/0", safe_div(7.0, 0.0));
return 0;
}
Example explained
Line 1safe_div returns the whole struct by value, so the flag travels with the number and no pointer argument is needed.
Line 2print_result is void, and its return inside the if is a plain early exit; the value printf itself returns is simply discarded.
Line 3The struct coming out of safe_div is a value, not a variable: safe_div(7.0, 2.0).ok = 1 would not compile, while passing it straight into print_result is fine.
Line 4r is a local of safe_div, yet returning it is safe because the caller gets a copy, unlike returning a pointer to r would be.
Important notes
main is the one function where falling off the end is safe: since C99 it behaves as return 0, and the value main returns becomes the process exit status, where only the low eight bits are visible to the shell on POSIX systems, so use 0 or EXIT_SUCCESS and EXIT_FAILURE rather than arbitrary numbers.
A returned value is not an lvalue, so &f() and f() = x are compile errors; store the result in a variable when you need to reuse or modify it.
Common mistakes
Letting one branch of a non-void function fall through to the closing brace, or writing a bare return; in an int function: it may only warn, and the caller then uses whatever was left in the return register, so the bug often shows up only in an optimised build.
Reusing 0 or -1 as the failure marker for a function that can legitimately compute 0 or -1, so "no matches" and "bad input" arrive as the same value and the caller silently trusts a wrong answer.
Inspecting errno without first checking that the call actually reported failure: successful calls do not reset errno, so a stale value from an earlier error makes good data look broken.
Try it yourself
Change, predict, then run
Write int last_index_of(const char *s, char c) that returns the index of the last occurrence of c or -1 when it is absent, then rewrite it as int last_index_of(const char *s, char c, int *out) that returns 1 or 0. Call both versions on "banana" with 'a' and with 'z' and print what each reports.
Open the C workspaceCheck your understanding
A function int count_matches(const char *text, char c) returns how many times c occurs, and returns -1 when text is NULL. Why is -1 a defensible failure signal here while 0 would not be?
- Because returning 0 always means success in C, so 0 is reserved for that meaning
- Because a negative return value makes the compiler set errno for the caller to inspect
- Because a count can never be negative, so -1 collides with no real answer, while 0 is an ordinary count
- Because -1 is true in an if test and 0 is false, so the caller can write if (count_matches(...))
Show answer
A sentinel only works if it lies outside the set of values the function can genuinely compute. Counts run from 0 upward, so -1 is unreachable and unambiguous, whereas 0 is the perfectly normal answer for "c never appears", leaving the caller unable to distinguish that from a NULL argument. The first option is tempting because main and many library calls use 0 for success, but that is a convention chosen per function, not a language rule, and it does not apply to a function whose return value is a count. Nothing about a negative return value touches errno, and truthiness would flip the meaning so that a real count of 0 looked like failure.