skip to content

In bash, a function ends with `return 42`. What can the caller actually do with that 42, and how would you get a non-numeric result such as a hostname out of a function instead?

level: juniorimportance: must knowfreq 68%

answer

  1. a number, not a value
  2. one byte, 0 to 255
  3. the caller reads $?
  4. data leaves on stdout
  5. capture with out=$(func)

basics

~20 s

In bash, return only sets the function's exit status — a number 0-255 the caller reads in $? — never a value. To hand back real data, print it with echo or printf and capture it with result=$(func).

solid answer

~40 s

`return 42` sets the function's **exit status** to 42, nothing more. The caller reads it from `$?`, or more usually just tests it: `if myfunc; then`, `myfunc || handle_error`. By convention 0 means success and non-zero means failure, and the value is a single byte — bash reduces anything larger modulo 256, so `return 256` arrives as 0, which reads as success. If a function has no explicit `return`, its status is that of the last command it ran, which is why a trailing `echo` can silently mask an earlier failure. Real data goes out on standard output instead: the function does `printf '%s\n' "$hostname"` and the caller does `host=$(myfunc)`. You can have both at once — `if host=$(myfunc); then …` gives you the captured value and the status in one call.

go deeper

for a junior

Know the one-line rule: return sets an exit status (0-255) that the caller reads in $?, and any real value has to be printed and captured with out=$(func). Saying that cleanly is most of the answer.

for a middle

Explain the mechanics: the status is a single byte so bash truncates modulo 256, a function with no return inherits the status of its last command, and a plain assignment from a command substitution carries the function's status into $?.

for a senior

Show the failure modes you have actually hit — a trailing echo resetting a function's status to 0, a count of errors returned as a status and wrapping to zero — and how you review shell libraries so status and payload never share a channel.

for a principal

Own the contract: a function library that mirrors the command interface (status for the verdict, stdout for the data) is composable and testable, while ad-hoc conventions force every caller to know each function's private protocol. Be ready to say when a script has outgrown that contract entirely.

## Two separate channels Every command bash runs — an external program, a builtin, or a shell function — finishes by handing the shell one small integer, its **exit status**. Bash stores the most recent one in the special parameter `$?`. By convention 0 means success and any non-zero value means some kind of failure. That integer is the only thing `return` can set. A function therefore has two completely different ways of talking to its caller, and confusing them is the most common bash-function bug there is: - the **exit status**, set by `return N` or by the last command the function ran, read with `$?` and tested by `if`, `while`, `&&` and `||`; - **standard output**, written with `echo` or `printf`, which the caller captures with command substitution: `value=$(func)`. ```bash lookup_ip() { printf '%s\n' "10.0.0.7" # the data return 0 # the verdict } ip=$(lookup_ip) # ip == 10.0.0.7 ``` This split is not an accident. Functions are meant to be indistinguishable from commands: `grep` prints its matches and returns 0 or 1, so it composes into `if`, into pipes and into `$( )`. A function that follows the same contract composes the same way; one that tries to "return a string" through `return` composes with nothing. ## `return` takes a number, and only a small one `return "$hostname"` does not encode a string into the status. Bash rejects a non-numeric argument with a `numeric argument required` message, and the function stops there with a failure status. Even a number is unsafe if it is large, because the status is a single byte and bash reduces the argument modulo 256: ```bash f() { return 300; }; f; echo "$?" # 44 g() { return 256; }; g; echo "$?" # 0 <-- a failure that reads as success ``` The practical rule that follows: never map a *count* — errors found, files skipped, rows imported — onto the exit status. Counts overflow, and the one value that overflows to zero is the one that means "lots of errors". Print the count instead, and reserve the status for the yes/no question "did this work?". ## Falling off the end of a function With no explicit `return`, a function's status is the status of the last command it executed. That is usually convenient and occasionally lethal: ```bash sync_build() { rsync -a ./build/ server:/srv/app/ # this may fail echo "sync step finished" # this succeeds, status 0 } sync_build || echo "never printed" ``` The trailing log line overwrote the status the caller cared about. Either end the function with the command whose status matters, or save it (`rc=$?`) and `return "$rc"` explicitly. ## Getting real data back The payload goes to standard output and the caller captures it: ```bash config_value() { printf '%s\n' "production"; } env_name=$(config_value) ``` A plain assignment whose right-hand side is a command substitution takes the substitution's exit status, so a single `if env_name=$(config_value); then …; else …; fi` gives you the value *and* the success test together. For more than one value, print one per line and have the caller read them, or hand the function the name of a variable to fill in. The price of this contract is that standard output is now reserved. Any stray progress message a function prints becomes part of the value the caller captured, so diagnostics have to be written to standard error instead — that is the whole reason the `>&2` convention exists in shell libraries. ## What interviewers listen for Say plainly that `return` is a *status*, not a *value*; name `$?`; name the 0-255 window and the modulo-256 truncation; and show that you know how to get both a value and a verdict out of one call with `if out=$(func)`. Candidates who reach for `return "$string"` or who read data out of `$?` are telling the interviewer they have only ever copied shell, not written it.

  • What happens if you write `return "$count"` and `$count` holds a non-numeric string?
    Bash refuses it: it prints a `numeric argument required` diagnostic and the function stops immediately with a non-zero status. The string is never passed to the caller in any form. If the value is genuinely data rather than a verdict, print it and let the caller capture it; keep `return` for numbers you chose deliberately.
  • How does one call get both the function's printed value and its success or failure?
    Use the assignment as the condition: `if out=$(myfunc); then … else …; fi`. A plain variable assignment from a command substitution adopts the substitution's exit status, so the `if` tests the function while `$out` holds what it printed. Testing `$?` on a separate line works too, as long as nothing else runs in between.
  • Why do people say a bash function should behave like a command?
    Because the shell's composition operators only understand the command contract: status for control flow, stdout for data. A function that honours it drops into `if`, `&&`, `||`, pipelines and `$( )` with no adapter. One that returns data through `$?` or a surprise global can only be called in the one way its author had in mind.

saying these in an interview costs you the question

  • Thinks return hands a string back to the caller
  • Expects return 256 or return 300 to arrive intact
  • Reads a function's output from $? instead of capturing it
  • Forgets that a function with no return inherits the last command's status
  • Maps an error count onto the exit status

context