Bash Functions Don't 'Return Values' Like Other Languages
In most languages, return sends a value of any type back to the caller. In Bash, return only sets a numeric exit status between 0 and 255, where 0 conventionally means success and any nonzero value means some kind of failure or specific condition. If you need to hand back actual data — a string, a number, a list — you must either print it to stdout and let the caller capture it with command substitution, or write it into a variable the caller can see (a global or a nameref).
Cricket analogy: An umpire's decision is binary at its core — out or not out — even though the reasoning behind it is complex; that single-bit-like signal is what return's numeric code communicates, not the full story.
Using `return`, `$?`, and Conditionals Correctly
return N (0-255) sets the function's exit status, and if return is omitted, the function's exit status is that of its last executed command. $? captures the exit status of the immediately preceding command, so it must be checked right away — any command run in between, including an echo for debugging, overwrites it. In practice, prefer testing the function call directly in a conditional (if my_func; then ...) over calling it and then checking $? separately, since the direct form is less error-prone.
Cricket analogy: Checking $? after running two more commands is like reviewing a bowler's figures after the next bowler has already bowled two overs — you're reading the wrong over's stats.
#!/usr/bin/env bash
set -uo pipefail
is_valid_port() {
local port=$1
if [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )); then
return 0 # success
else
return 1 # failure
fi
}
# Prefer testing the call directly:
if is_valid_port "8080"; then
echo "8080 is valid"
fi
# Returning data: print to stdout, caller captures it
get_free_port() {
local port=$(( (RANDOM % 1000) + 8000 ))
echo "$port" # data goes to stdout
return 0 # status goes through exit code
}
chosen_port=$(get_free_port)
echo "Chosen port: $chosen_port"
Distinguishing 'Data Output' From 'Status'
A well-designed function keeps its two channels separate: stdout for data the caller wants to capture, and the exit status for success/failure signaling. Mixing them — for example printing both a result and a debug message to stdout — corrupts the captured value when the caller does result=$(my_func). Debug or progress messages that shouldn't be captured belong on stderr (echo "debug" >&2), keeping stdout clean for exactly the data the function intends to return.
Cricket analogy: A commentary broadcast mixing the actual scorecard update with the commentator's personal opinions in the same feed would corrupt an automated scoreboard reading only the numbers — separate the feeds, like stdout versus stderr.
Redirect diagnostic or progress output to stderr with >&2 inside any function whose stdout is meant to be captured: echo "Fetching config..." >&2. This keeps result=$(my_func) clean while still letting a human watching the terminal see progress messages, since stderr is not redirected by command substitution.
local swallows exit codes when combined with command substitution on the same line (see 'Local vs Global Variables' for detail): local x=$(cmd) always reports the exit status of local, not of cmd. This directly affects return-value checking — declare the variable first, assign second, if you need to act on cmd's real exit status.
return Nonly sets a numeric exit status (0-255); it cannot return arbitrary data types.- 0 means success by convention; any nonzero value signals failure or a specific condition.
- Use stdout + command substitution to return actual data from a function.
$?reflects only the immediately preceding command — check it before running anything else.- Prefer
if my_func; then ...over calling then separately checking$?. - Route debug/progress messages to stderr (
>&2) to keep stdout clean for captured data. - Combine data output (stdout) and status (exit code) deliberately — never conflate them.
Practice what you learned
1. What does `return 1` actually communicate from a Bash function?
2. How should a function communicate an actual computed value (like a string) back to its caller?
3. Why is checking `$?` right after an `echo` debug statement unreliable?
4. Where should progress/debug messages be sent inside a function whose stdout is meant to be captured?
5. What is generally the safer style for checking a function's success?
Was this page helpful?
You May Also Like
Advanced Bash Functions
Go beyond basic function syntax to master argument handling, scoping, namerefs, and composition patterns used in production Bash scripts.
Local vs Global Variables
Understand how Bash variable scope actually works, why globals are the default, and how to use `local` deliberately to write safer scripts.
Recursive Bash Functions
Learn how to write Bash functions that call themselves safely, including base cases, stack limits, and when recursion is (and isn't) the right tool.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics