This guide explores different ways to structure zsh scripts and functions when you want to exit early from inside a nested function. The main pitfall is that exit inside a sourced script or function doesn’t just stop the function — it can terminate your whole shell session. The examples here show common patterns people try, the problems they run into, and safer alternatives. Each section highlights what happens when you run or source the code, where things can go wrong, and why the simplest approach — a basic standalone script — is usually still the best.
- 1. Simple Script (no special handling)
- 2. Export Function or Run When Executed
- 2.5. Encapsulated Helpers (no global leaks)
- 3. Function with Embedded Subshell
- 4. Function Using
ERR_RETURN - Recommendations
- Caveat
File: my_script_name.zsh
#!/usr/bin/env zsh
show_help() {
echo "Help: running show_help"
exit 0
}
error_exit() {
echo "Error: $1" >&2
exit 1
}
echo "Starting simple script"
show_help
echo "This should never run"Behavior:
- When executed (
./my_script_name.zsh):
then exits with codeStarting simple script Help: running show_help0. - When sourced (
. my_script_name.zsh):exitkills the current shell session.
Issue: Not safe to source, since exit is used inside helper functions.
#!/usr/bin/env zsh
show_help() {
echo "Help: doing help"
exit 0
}
error_exit() {
echo "Error: $1" >&2
exit 1
}
_my_script_name() {
echo "Some main content"
show_help
echo "This should never run"
}
# Run the main function if script is not sourced
if ! [[ "$ZSH_EVAL_CONTEXT" == *:file ]]; then
_my_script_name "$@"
fiBehavior:
- Sourced: defines
_my_script_name,show_help,error_exit, but does not run. - Executed: runs
_my_script_nameimmediately.
Issues:
- Unsafe to source, because
show_helpanderror_exitboth callexit. If another script sources this one and then calls_my_script_name, anexitinside these helpers will terminate the caller’s shell. - Leaks the helper functions (
show_helpanderror_exit) into the global namespace. They can be called directly, but if their names clash with other functions or commands, problems will occur.
Workaround:
- A caller could safely run it in a subshell:
ensuring any
( _my_script_name )
exitcalls only terminate the subshell.
#!/usr/bin/env zsh
_my_script_name() {
show_help() {
echo "Help: doing help"
exit 0
}
error_exit() {
echo "Error: $1" >&2
exit 1
}
echo "Some main content"
show_help
echo "This should never run"
}
# Run the main function if script is not sourced
if ! [[ "$ZSH_EVAL_CONTEXT" == *:file ]]; then
_my_script_name "$@"
fiBehavior:
- Sourced: defines only
_my_script_name, not the helpers. - Executed: runs
_my_script_nameimmediately.
Notes:
- By defining
show_helpanderror_exitinside_my_script_name, we avoid polluting the global namespace. Only_my_script_nameis exposed. - Still unsafe to source if
exitcalls are triggered by helpers, but avoids the name clash problem.
Workaround:
- A caller could safely run it in a subshell:
ensuring any
( _my_script_name )
exitcalls only terminate the subshell.
_my_script_name() {
# Wrap the entire script in a subshell so we can use exit cleanly
(
show_help() {
echo "Help: doing help"
exit 0
}
error_exit() {
echo "Error: $1" >&2
exit 1
}
echo "Some main content"
show_help
echo "This should never run"
)
return $?
}
alias my_script_name="_my_script_name"Behavior:
- Can be run directly in the terminal without causing it to exit.
- Entire function runs in a subshell, so
exitonly terminates the subshell. - Parent shell continues running, with return code propagated.
Notes:
- This can be a useful hack if you want to define a more complex “script-like” function to be used in an alias but don’t want to make it an actual script.
- But seriously: save yourself hours of headaches, hacks, and workarounds — don’t try and do things like this example, just use a proper script. You’ll thank yourself later.
Caveat:
- Variable changes inside the subshell are lost (not visible to parent).
_my_script_name() {
setopt localoptions ERR_RETURN
show_help() {
echo "Help: doing help"
return 1 # must be non-zero to trigger ERR_RETURN
}
error_exit() {
echo "Error: $1" >&2
return 2
}
echo "Some main content"
show_help
echo "This should never run"
}
alias my_script_name="_my_script_name"Behavior:
ERR_RETURNcauses any non-zero return to immediately exit the function (similar toset -ebut scoped to the function).show_helpnow returns an error code instead of exiting. Returning1triggers ERR_RETURN and halts the function early.
Notes:
- Unlike earlier examples,
exithas been swapped forreturnwith error codes. This avoids killing the whole shell and makes the function safe to run inline. - For this to work,
show_helpcannot return0. It must return a non-zero value to trigger the ERR_RETURN behavior. - As with the subshell hack above: this can be useful if you want “script-like” behavior in a function/alias.
- But seriously: save yourself hours of headaches, hacks, and workarounds — don’t try and do things like this example, just use a proper script. You’ll thank yourself later.
- Strongly recommend just using pattern 1 (a basic script) for most cases. It is the simplest, safest, and least surprising approach.
- If you really need to be able to call it both as a script or source it, then maybe pattern 2.5 is fine, since it avoids polluting the global scope.
- Pattern 4 is kind of hacky, but if for some reason you can’t use a normal script and must do it only as a function, it’s probably less hacky than pattern 3. But seriously… just don’t.
- Pattern 3 is also hacky, though it does technically work. But again — seriously, just don’t.
But seriously, tying this back to the strong recommendation above: stop trying to think of esoteric clever ways to do things, or being lazy and hacking a function+alias into a full-blown script. Just create a new script in your bin directory so you don’t waste a day chasing esoteric rabbit holes only to end up back at the most obvious simple solution still being the best.
Trust me. Seriously.. Just do the basic shell script.. Save yourself.. I beg you!
I don’t claim that everything here is perfectly accurate, complete, or the best possible way to handle these patterns. There may be cleaner workarounds, safer idioms, or zsh features I’ve overlooked. This write-up is simply my attempt to capture what I learned after burning a day chasing hacks, dead ends, and confusing behavior around exit inside nested functions. My goal is to document the trade-offs and pitfalls clearly enough that I (and hopefully others) don’t have to waste that time again.