A shared bash library defines `require_tool() { command -v "$1" >/dev/null || { echo "missing: $1" >&2; exit 1; }; }`. What goes wrong when another script sources that library and calls the function, and what should the function do instead?
answer
- one ends a function, one ends the shell
- the caller's else branch never runs
- sourcing means it is your shell
- libraries report, main decides
- one die() at the top level
basics
~20 sexit terminates the whole shell running the function, not just the function, so the caller dies at the call site and can never fall back or clean up. A library function should return 1 after writing the reason to standard error, leaving the decision to the caller.
solid answer
~50 s`return` ends the function; `exit` ends the shell process executing it. Because a sourced library runs in the caller's own shell, `exit 1` inside `require_tool` kills the calling script outright — `if require_tool jq; then … else use_python; fi` never reaches the `else`, a wrapper that wanted to try three candidates gets one, and someone sourcing the library in a terminal to test it loses the terminal. The blast radius also depends on the call site: if the same function is invoked inside a command substitution or one side of a pipeline, `exit` only ends that subshell and the script carries on as if nothing happened, so the behaviour is inconsistent as well as rude. The rule is that library functions `return` a non-zero status and describe the problem on standard error; only top-level script code, typically one `die()` in `main`, decides to `exit`.
code
bash · 15 lines#!/usr/bin/env bash
require_tool() {
if ! command -v "$1" >/dev/null 2>&1; then
printf 'missing tool: %s\n' "$1" >&2
return 1 # not exit 1: the caller decides
fi
}
if require_tool jq; then
parser=jq
else
parser=python3
fi
printf 'using %s\n' "$parser"go deeper
Learn the one-sentence difference: return ends the function and hands back a status, while exit ends the entire shell running it. In a script you are reading, exit inside a helper means the script stops there.
Explain why a sourced library changes the stakes — the function is running in the caller's shell, so its exit is the caller's exit — and rewrite the helper to return non-zero after logging to standard error.
Show the operational consequences: fallback branches never taken, prerequisite checks that stop at the first failure, a test harness that dies mid-suite, and the same call behaving differently when it happens to run in a subshell.
Frame it as an ownership rule for the team's shell libraries — helpers report, the entry point decides, and exactly one place in the codebase terminates the process. It is the same argument as forbidding a library from killing the host process in any language.
## Two different verbs `return` leaves the current function and sets its exit status. `exit` terminates the shell that is executing the code — the whole process, with whatever status you gave it. Inside a function those are not "the same thing with different scope": one is a local control transfer, the other is process termination. The distinction is invisible while a script is a single file that runs its functions and then finishes. It becomes very visible the moment functions are packaged as a library that other code sources, because a sourced file runs in the caller's shell — so the shell `exit` terminates *is* the caller's. ## What the caller loses ```bash source ./lib/checks.sh if require_tool jq; then parser=jq else parser=python3 # unreachable if require_tool calls exit fi ``` With `exit 1` in the function, several things the caller was entitled to simply never happen: - **The fallback branch.** The `else` is dead code. So is any `||` alternative and any retry loop. - **Error aggregation.** A script that wanted to check ten prerequisites and report all the missing ones stops at the first. - **Contextual messaging.** The library's terse `missing: jq` is all the user sees; the caller never gets to add "required by the report step — install with `apt install jq`". - **Its own status choice.** The script exits 1 even if this condition was supposed to mean "skip this optional step" and finish successfully. One thing that *does* still happen is any `trap ... EXIT` the caller installed: an EXIT trap fires on a normal exit, so temp-file cleanup still runs. That is small consolation when the run itself was cut short. And there is a second-order surprise: someone who sources the library in an interactive session — the natural way to poke at a function — loses their terminal when the function decides to exit. ## The blast radius depends on the call site Because `exit` terminates the shell *executing* the function, and some call sites run the function in a child shell, the same code produces two opposite outcomes: ```bash require_tool jq # kills the script ver=$(require_tool jq) # kills only the subshell; the script continues require_tool jq | tee log # likewise; the script continues ``` A function whose failure handling depends on whether the caller happened to capture its output is not a function anyone can reason about. ## The convention that works Split responsibility by layer: - **Library functions report; they do not decide.** Write the diagnosis to standard error, `return` a non-zero status, and stop there. Distinct codes can carry meaning (1 = missing, 2 = wrong version) as long as they stay within 0-255. - **The top-level script decides.** Keep one `die() { printf '%s\n' "$*" >&2; exit 1; }` and call it from `main`, where the script actually knows whether a failure is fatal. ```bash require_tool() { command -v "$1" >/dev/null 2>&1 && return 0 printf 'missing tool: %s\n' "$1" >&2 return 1 } main() { require_tool jq || die "install jq before running the deploy" } ``` Now the function composes: it works under `if`, under `||`, inside a loop that collects every missing tool, and inside a test harness that asserts on the status without dying. ## Small print worth knowing `return` is only legal inside a function or in a file being sourced; using it at the top level of an executed script is an error rather than a silent no-op. And `return` with no argument yields the status of the last command executed, which is occasionally what you want and frequently an accident — write the number you mean. ## What the interviewer is testing Not the definition of `exit`. They want to hear that you have thought about *who owns the decision to stop*, and that you recognise a library that takes that decision away from its callers as a design defect — the shell equivalent of a library that calls `System.exit` instead of throwing.
- Does `exit` inside a function always end the whole script?No — it ends the shell that is running the function, and some call sites run it in a child shell. Called inside a command substitution or as one stage of a pipeline, the function's `exit` terminates only that child, and the parent script continues with a mere non-zero status. Same code, opposite outcome depending on how it was called.
- Is there any case where a function legitimately calls exit?Yes, in top-level script code that owns the run: a `die()` helper, or the last lines of `main`. The test is ownership — if the file is meant to be sourced by other people's scripts, it does not get to end their process. Keep the exit decision in exactly one place, near the entry point.
- What is the status of a bare `return` with no argument?It is the exit status of the last command executed in the function, exactly as if the function had fallen off its end. That is fine when the last command is the one whose success matters, and a source of silent bugs when a log line or an assignment ran after it. Write the number you mean.
saying these in an interview costs you the question
- Thinks exit inside a function only ends the function
- Believes exit in a sourced library affects just the library
- Uses exit in shared helpers so callers "cannot forget to handle it"
- Cannot say why an if/else around the call never reaches else
- Assumes exit and return differ only in the status they set