100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Bash

Function Return Values and Exit Codes

Learn how Bash functions actually communicate results — via numeric exit codes, stdout capture, and namerefs — and how to check them reliably.

Functions & ScopeIntermediate9 min readJul 10, 2026
Analogies

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.

bash
#!/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 N only 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

Was this page helpful?

Topics covered

#Bash#AdvancedBashScriptingStudyNotes#DevOps#FunctionReturnValuesAndExitCodes#Function#Return#Values#Exit#Functions#StudyNotes#SkillVeris