Bash: Error handling
Contents
- 1 Problem
- 2 Solutions
- 3 Caveat 1: `Exit on error' ignoring subshell exit status
- 4 Caveat 2: `Exit on error' not exitting subshell on error
- 5 Caveat 3: `Exit on error' not exitting command substition on error
- 6 The tools
- 7 Solutions revisited: Combining the tools
- 8 Test
- 9 See also
- 10 Journal
- 11 Comments
Problem
I want to catch errors in bash script using set -e
(or set -o errexit
or trap ERR
). What are best practices?
To -e or not to to -e?
Opinions differ about whether it's wise to use Doubtful about -e: |
Solutions
See #Solutions revisited: Combining the tools for detailed explanations.
If the script is executed in a subshell, it's relative easy: You don't have to worry about backing up and restoring shell options and shell traps, because they're automatically restored when you exit the subshell.
Executed in subshell, exit on error
Example script:
#!/bin/bash -eu # -e: Exit immediately if a command exits with a non-zero status. # -u: Treat unset variables as an error when substituting. (false) # Caveat 1: If an error occurs in a subshell, it isn't detected (false) || false # Solution: If you want to exit, you have to detect the error yourself (false; true) || false # Caveat 2: The return status of the ';' separated list is `true' (false && true) || false # Solution: If you want to control the last command executed, use `&&'
See also #Caveat 1: `Exit on error' ignoring subshell exit status
Executed in subshell, trap error
#!/bin/bash -Eu # -E: ERR trap is inherited by shell functions. # -u: Treat unset variables as an error when substituting. # # Example script for handling bash errors. Exit on error. Trap exit. # This script is supposed to run in a subshell. # See also: http://fvue.nl/wiki/Bash:_Error_handling # Trap non-normal exit signals: 1/HUP, 2/INT, 3/QUIT, 15/TERM, ERR trap onexit 1 2 3 15 ERR #--- onexit() ----------------------------------------------------- # @param $1 integer (optional) Exit status. If not set, use `$?' function onexit() { local exit_status=${1:-$?} echo Exiting $0 with $exit_status exit $exit_status } # myscript # Allways call `onexit' at end of script onexit
Caveat 1: `Exit on error' ignoring subshell exit status
The `-e' setting does not exit if an error occurs within a subshell, for example with these subshell commands: (false)
or bash -c false
Example script caveat1.sh:
#!/bin/bash -e echo begin (false) echo end
Executing the script above gives:
$ ./caveat1.sh begin end $
Conclusion: the script didn't exit after (false).
Solution: Generate error yourself if subshell fails
( SHELL COMMANDS ) || false
In the line above, the exit status of the subshell is checked. The subshell must exit with a zero status - indicating success, otherwise `false' will run, generating an error in the current shell.
Note that within a bash `list', with commands separated by a `;', the return status is the exit status of the last command executed. Use the control operators `&&' and `||' if you want to control the last command executed:
$ (false; true) || echo foo $ (false && true) || echo foo foo $
Example 1
Example script example.sh:
#!/bin/bash -e echo begin (false) || false echo end
Executing the script above gives:
$ ./example.sh begin $
Conclusion: the script exits after false.
Example 2
Example bash commands:
$ trap 'echo error' ERR # Set ERR trap $ false # Non-zero exit status will be trapped error $ (false) # Non-zero exit status within subshell will not be trapped $ (false) || false # Solution: generate error yourself if subshell fails error $ trap - ERR # Reset ERR trap
Caveat 2: `Exit on error' not exitting subshell on error
The `-e' setting doesn't always immediately exit the subshell `(...)' when an error occurs. It appears the subshell behaves as a simple command and has the same restrictions as `-e':
- Exit immediately if a simple command exits with a non-zero status, unless the subshell is part of the command list immediately following a `while' or `until' keyword, part of the test in an `if' statement, part of the right-hand-side of a `&&' or `||' list, or if the command's return status is being inverted using `!'
Example script caveat2.sh:
#!/bin/bash -e (false; echo A) # Subshell exits after `false' !(false; echo B) # Subshell doesn't exit after `false' true && (false; echo C) # Subshell exits after `false' (false; echo D) && true # Subshell doesn't exit after `false' (false; echo E) || false # Subshell doesn't exit after `false' if (false; echo F); then true; fi # Subshell doesn't exit after `false' while (false; echo G); do break; done # Subshell doesn't exit after `false' until (false; echo H); do break; done # Subshell doesn't exit after `false'
Executing the script above gives:
$ ./caveat2.sh B D E F G H
Solution: Use logical operators (&&, ||) within subshell
Use logical operators `&&' or `||' to control execution of commands within a subshell.
Example
#!/bin/bash -e (false && echo A) !(false && echo B) true && (false && echo C) (false && echo D) && true (false && echo E) || false if (false && echo F); then true; fi while (false && echo G); do break; done until (false && echo H); do break; done
Executing the script above gives no output:
$ ./example.sh $
Conclusion: the subshells do not output anything because the `&&' operator is used instead of the command separator `;' as in caveat2.sh.
Caveat 3: `Exit on error' not exitting command substition on error
The `-e' setting doesn't immediately exit command substitution when an error occurs, except when bash is in posix mode:
$ set -e $ echo $(false; echo A) A
Solution 1: Use logical operators (&&, ||) within command substitution
$ set -e $ echo $(false || echo A)
Solution 2: Enable posix mode
When posix mode is enabled via set -o posix
, command substition will exit if `-e' has been set in the
parent shell.
$ set -e $ set -o posix $ echo $(false; echo A)
Enabling posix might have other effects though?
The tools
Exit on error
Bash can be told to exit immediately if a command fails. From the bash manual ("set -e"):
- "Exit immediately if a simple command (see SHELL GRAMMAR above) exits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of a && or || list, or if the command's return value is being inverted via !. A trap on ERR, if set, is executed before the shell exits."
To let bash exit on error, different notations can be used:
- Specify `bash -e' as shebang interpreter
- Start shell script with `bash -e'
- Use `set -e' in shell script
- Use `set -o errexit' in shell script
- Use `trap exit ERR' in shell script
Specify `bash -e' as the shebang interpreter
You can add `-e' to the shebang line, the first line of your shell script:
#!/bin/bash -e
This will execute the shell script with `-e' active. Note `-e' can be overridden by invoking bash explicitly (without `-e'):
$ bash shell_script
Example
Create this shell script example.sh and make it executable with chmod u+x example.sh
:
#!/bin/bash -e echo begin false # This should exit bash because `false' returns error echo end # This should never be reached
Example run:
$ ./example.sh begin $ bash example.sh begin end $
Set ERR trap to exit
By setting an ERR trap you can catch errors as well:
trap command ERR
By setting the command to `exit', bash exits if an error occurs.
trap exit ERR
Example
Example script example.sh
#!/bin/bash trap exit ERR echo begin false echo end
Example run:
$ ./example.sh begin $
The non-zero exit status of `false' is catched by the error trap. The error trap exits and `echo end' is never reached.
Solutions revisited: Combining the tools
Executed in subshell, trap on exit
#!/bin/bash # --- subshell_trap.sh ------------------------------------------------- # Example script for handling bash errors. Exit on error. Trap exit. # This script is supposed to run in a subshell. # See also: http://fvue.nl/wiki/Bash:_Error_handling # Let shell functions inherit ERR trap. Same as `set -E'. set -o errtrace # Trigger error when expanding unset variables. Same as `set -u'. set -o nounset # Trap non-normal exit signals: 1/HUP, 2/INT, 3/QUIT, 15/TERM, ERR # NOTE1: - 9/KILL cannot be trapped. #+ - 0/EXIT isn't trapped because: #+ - with ERR trap defined, trap would be called twice on error #+ - with ERR trap defined, syntax errors exit with status 0, not 2 # NOTE2: Setting ERR trap does implicit `set -o errexit' or `set -e'. trap onexit 1 2 3 15 ERR #--- onexit() ----------------------------------------------------- # @param $1 integer (optional) Exit status. If not set, use `$?' function onexit() { local exit_status=${1:-$?} echo Exiting $0 with $exit_status exit $exit_status } # myscript # Allways call `onexit' at end of script onexit
Rationale
+-------+ +----------+ +--------+ +------+ | shell | | subshell | | script | | trap | +-------+ +----------+ +--------+ +------+ : : : : +-+ +-+ +-+ error +-+ | | | | | |-------->| | | | exit | | | ! | | | |<-----------------------------------+ +-+ : : : : : : :
Figure 1. Trap in executed script
When a script is executed from a shell, bash will create a subshell in which the script is run. If a trap catches an error, and the trap says `exit', this will cause the subshell to exit.
Sourced in current shell
If the script is sourced (included) in the current shell, you have to worry about restoring shell options and shell traps. If they aren't restored, they might cause problems in other programs which rely on specific settings.
#!/bin/bash #--- listing6.inc.sh --------------------------------------------------- # Demonstration of ERR trap being reset by foo_deinit() with the use # of `errtrace'. # Example run: # # $ set +o errtrace # Make sure errtrace is not set (bash default) # $ trap - ERR # Make sure no ERR trap is set (bash default) # $ . listing6.inc.sh # Source listing6.inc.sh # $ foo # Run foo() # foo_init # Entered `trap-loop' # trapped # This is always executed - with or without a trap occurring # foo_deinit # $ trap # Check if ERR trap is reset. # $ set -o | grep errtrace # Check if the `errtrace' setting is... # errtrace off # ...restored. # $ # # See: http://fvue.nl/wiki/Bash:_Error_handling function foo_init { echo foo_init fooOldErrtrace=$(set +o | grep errtrace) set -o errtrace trap 'echo trapped; break' ERR # Set ERR trap } function foo_deinit { echo foo_deinit trap - ERR # Reset ERR trap eval $fooOldErrtrace # Restore `errtrace' setting unset fooOldErrtrace # Delete global variable } function foo { foo_init # `trap-loop' while true; do echo Entered \`trap-loop\' false echo This should never be reached because the \`false\' above is trapped break done echo This is always executed - with or without a trap occurring foo_deinit }
Todo
- an existing ERR trap must be restored and called
- test if the `trap-loop' is reached if the script breaks from a nested loop
Rationale
`Exit' trap in sourced script
When the script is sourced in the current shell, it's not possible to use `exit' to terminate the program: This would terminate the current shell as well, as shown in the picture underneath.
+-------+ +--------+ +------+ | shell | | script | | trap | +-------+ +--------+ +------+ : : : +-+ +-+ error +-+ | | | |-------->| | | | | | | | | | exit | | | | <------------------------------------------+ : : :
Figure 2. `Exit' trap in sourced script
When a script is sourced from a shell, bash will run the script in the current shell. If a trap catches an error, and the trap says `exit', this will cause the current shell to exit.
`Break' trap in sourced script
A solution is to introduce a main loop in the program, which is terminated by a `break' statement within the trap.
+-------+ +--------+ +--------+ +------+ | shell | | script | | `loop' | | trap | +-------+ +--------+ +--------+ +------+ : : : : +-+ +-+ +-+ error +-+ | | | | | |------->| | | | | | | | | | | | | | break | | | | | | return | |<----------------------+ | |<----------+ : : +-+ : : : : : : :
Figure 3. `Break' trap in sourced script
When a script is sourced from a shell, e.g. . ./script
, bash will run the script in the current shell. If a trap catches an error, and the trap says `break', this will cause the `loop' to break and to return to the script.
For example:
#!/bin/bash #--- listing3.sh ------------------------------------------------------- # See: http://fvue.nl/wiki/Bash:_Error_handling trap 'echo trapped; break' ERR; # Set ERR trap function foo { echo foo; false; } # foo() exits with error # `trap-loop' while true; do echo Entered \`trap-loop\' foo echo This is never reached break done echo This is always executed - with or without a trap occurring trap - ERR # Reset ERR trap
Listing 3. `Break' trap in sourced script
When a script is sourced from a shell, e.g. ./script
, bash will run the script in the current shell. If a trap catches an error, and the trap says `break', this will cause the `loop' to break and to return to the script.
Example output:
$> source listing3.sh Entered `trap-loop' foo trapped This is always executed after a trap $>
Trap in function in sourced script without `errtrace'
A problem arises when the trap is reset from within a function of a sourced script. From the bash manual, set -o errtrace
or set -E
:
If set, any trap on `ERR' is inherited by shell functions, command
substitutions, and commands executed in a subshell environment.
The `ERR' trap is normally not inherited in such cases.
So with errtrace
not set, a function does not know of any `ERR' trap set, and thus the function is unable to reset the `ERR' trap. For example, see listing 4 underneath.
#!/bin/bash #--- listing4.inc.sh --------------------------------------------------- # Demonstration of ERR trap not being reset by foo_deinit() # Example run: # # $> set +o errtrace # Make sure errtrace is not set (bash default) # $> trap - ERR # Make sure no ERR trap is set (bash default) # $> . listing4.inc.sh # Source listing4.inc.sh # $> foo # Run foo() # foo_init # foo # foo_deinit # This should've reset the ERR trap... # $> trap # but the ERR trap is still there: # trap -- 'echo trapped' ERR # $> trap # See: http://fvue.nl/wiki/Bash:_Error_handling function foo_init { echo foo_init trap 'echo trapped' ERR;} # Set ERR trap function foo_deinit { echo foo_deinit trap - ERR ;} # Reset ERR trap function foo { foo_init echo foo foo_deinit ;}
Listing 4. Trap in function in sourced script
foo_deinit()
is unable to unset the ERR trap, because errtrace
is not set.
Trap in function in sourced script with 'errtrace'
The solution is to set -o errtrace
. See listing 5 underneath:
#!/bin/bash #--- listing5.inc.sh --------------------------------------------------- # Demonstration of ERR trap being reset by foo_deinit() with the use # of `errtrace'. # Example run: # # $> set +o errtrace # Make sure errtrace is not set (bash default) # $> trap - ERR # Make sure no ERR trap is set (bash default) # $> . listing5.inc.sh # Source listing5.inc.sh # $> foo # Run foo() # foo_init # foo # foo_deinit # This should reset the ERR trap... # $> trap # and it is indeed. # $> set +o | grep errtrace # And the `errtrace' setting is restored. # $> # # See: http://fvue.nl/wiki/Bash:_Error_handling function foo_init { echo foo_init fooOldErrtrace=$(set +o | grep errtrace) set -o errtrace trap 'echo trapped' ERR # Set ERR trap } function foo_deinit { echo foo_deinit trap - ERR # Reset ERR trap eval($fooOldErrtrace) # Restore `errtrace' setting fooOldErrtrace= # Delete global variable } function foo { foo_init echo foo foo_deinit ;}
`Break' trap in function in sourced script with `errtrace'
Everything combined in listing 6 underneath:
#!/bin/bash #--- listing6.inc.sh --------------------------------------------------- # Demonstration of ERR trap being reset by foo_deinit() with the use # of `errtrace'. # Example run: # # $> set +o errtrace # Make sure errtrace is not set (bash default) # $> trap - ERR # Make sure no ERR trap is set (bash default) # $> . listing6.inc.sh # Source listing6.inc.sh # $> foo # Run foo() # foo_init # Entered `trap-loop' # trapped # This is always executed - with or without a trap occurring # foo_deinit # $> trap # Check if ERR trap is reset. # $> set -o | grep errtrace # Check if the `errtrace' setting is... # errtrace off # ...restored. # $> # # See: http://fvue.nl/wiki/Bash:_Error_handling function foo_init { echo foo_init fooOldErrtrace=$(set +o | grep errtrace) set -o errtrace trap 'echo trapped; break' ERR # Set ERR trap } function foo_deinit { echo foo_deinit trap - ERR # Reset ERR trap eval $fooOldErrtrace # Restore `errtrace' setting unset fooOldErrtrace # Delete global variable } function foo { foo_init # `trap-loop' while true; do echo Entered \`trap-loop\' false echo This should never be reached because the \`false\' above is trapped break done echo This is always executed - with or without a trap occurring foo_deinit }
Test
#!/bin/bash # Tests # An erroneous command should have exit status 127. # The erroneous command should be trapped by the ERR trap. #erroneous_command # A simple command exiting with a non-zero status should have exit status #+ <> 0, in this case 1. The simple command is trapped by the ERR trap. #false # Manually calling 'onexit' #onexit # Manually calling 'onexit' with exit status #onexit 5 # Killing a process via CTRL-C (signal 2/SIGINT) is handled via the SIGINT trap # NOTE: `sleep' cannot be killed via `kill' plus 1/SIGHUP, 2/SIGINT, 3/SIGQUIT #+ or 15/SIGTERM. #echo $$; sleep 20 # Killing a process via 1/SIGHUP, 2/SIGQUIT, 3/SIGQUIT or 15/SIGTERM is #+ handled via the respective trap. # NOTE: Unfortunately, I haven't found a way to retrieve the signal number from #+ within the trap function. echo $$; while true; do :; done # A syntax error is not trapped, but should have exit status 2 #fi # An unbound variable is not trapped, but should have exit status 1 # thanks to 'set -u' #echo $foo # Executing `false' within a function should exit with 1 because of `set -E' #function foo() { # false # true #} # foo() #foo echo End of script # Allways call 'onexit' at end of script onexit
See also
- Bash: Err trap not reset
- Solution for
trap - ERR
not resetting ERR trap.
Journal
20240522
function check_env() { for c in foo bar do which $c /dev/null || clean_exit 1 "Error: $_: command not found" done } # Wrapper around 'exit' to cleanup on exit. # @param $1 integer Exit status. If $1 is undefined or empty string (''), # exit status of global variable 'EXIT_STATUS' is used. If # neither $1 or 'EXIT_STATUS' set, exit with status 0 # (success). # @param $2 string (optional) Text to display on stderr as error message. function clean_exit() { EXIT_STATUS=${1:-${EXIT_STATUS:-0}} # Show possible error message on stderr [ $# -ge 2 ] && echo "$2" >&2 # Is 'clean_exit()' called recursive? if [[ $clean_exitting != 1 ]]; then export clean_exitting=1 # More cleanup fi exit $EXIT_STATUS }
20210114
Another caveat: exit (or an error-trap) executed within "process substitution" doesn't end outer process. The script underneath keeps outputting "loop1":
#!/bin/bash # This script outputs "loop1" forever, while I hoped it would exit all while-loops set -o pipefail set -Eeu while true; do echo loop1 while read FOO; do echo loop2 echo FOO: $FOO done < <( exit 1 ) done
The '< <()' notation is called process substitution.
See also:
- https://mywiki.wooledge.org/ProcessSubstitution
- https://unix.stackexchange.com/questions/128560/how-do-i-capture-the-exit-code-handle-errors-correctly-when-using-process-subs
- https://superuser.com/questions/696855/why-doesnt-a-bash-while-loop-exit-when-piping-to-terminated-subcommand
Workaround: Use "Here Strings" ([n]<<<word):
#!/bin/bash # This script will exit correctly if building up $rows results in an error set -Eeu rows=$(exit 1) while true; do echo loop1 while read FOO; do echo loop2 echo FOO: $FOO done <<< "$rows" done
20060524
#!/bin/bash #--- traptest.sh -------------------------------------------- # Example script for trapping bash errors. # NOTE: Why doesn't this scripts catch syntax errors? # Exit on all errors set -e # Trap exit trap trap_exit_handler EXIT # Handle exit trap function trap_exit_handler() { # Backup exit status if you're interested... local exit_status=$? # Change value of $? true echo $? #echo trap_handler $exit_status } # trap_exit_handler() # An erroneous command will trigger a bash error and, because # of 'set -e', will 'exit 127' thus falling into the exit trap. #erroneous_command # The same goes for a command with a false return status #false # A manual exit will also fall into the exit trap #exit 5 # A syntax error isn't catched? fi # Disable exit trap trap - EXIT exit 0
Normally, a syntax error exits with status 2, but when both 'set -e' and 'trap EXIT' are defined, my script exits with status 0. How can I have both 'errexit' and 'trap EXIT' enabled, *and* catch syntax errors via exit status? Here's an example script (test.sh):
set -e trap 'echo trapped: $?' EXIT fi $> bash test.sh; echo \$?: $? test.sh: line 3: syntax error near unexpected token `fi' trapped: 0 $?: 0
More trivia:
- With the line '#set -e' commented, bash traps 258 and returns an exit status of 2:
trapped: 258 $?: 2
- With the line '#trap 'echo trapped $?' EXIT' commented, bash returns an exit status of 2:
$?: 2
- With a bogus function definition on top, bash returns an exit status of 2, but no exit trap is executed:
function foo() { foo=bar } set -e trap 'echo trapped: $?' EXIT fi
fred@linux:~>bash test.sh; echo \$?: $? test.sh: line 4: syntax error near unexpected token `fi' test.sh: line 4: `fi' $?: 2
20060525
Writing Robust Bash Shell Scripts
#!/bin/bash #--- cleanup.sh --------------------------------------------------------------- # Example script for trapping bash errors. # NOTE: Use 'cleanexit [status]' instead of 'exit [status]' # Trap not-normal exit signals: 1/HUP, 2/INT, 3/QUIT, 15/TERM # @see catch_sig() trap catch_sig 1 2 3 15 # Trap errors (simple commands exiting with a non-zero status) # @see catch_err() trap catch_err ERR #--- cleanexit() -------------------------------------------------------------- # Wrapper around 'exit' to cleanup on exit. # @param $1 integer Exit status. If $1 not defined, exit status of global #+ variable 'EXIT_STATUS' is used. If neither $1 or #+ 'EXIT_STATUS' defined, exit with status 0 (success). function cleanexit() { echo "Exiting with ${1:-${EXIT_STATUS:-0}}" exit ${1:-${EXIT_STATUS:-0}} } # cleanexit() #--- catch_err() -------------------------------------------------------------- # Catch ERR trap. # This traps simple commands exiting with a non-zero status. # See also: info bash | "Shell Builtin Commands" | "The Set Builtin" | "-e" function catch_err() { local exit_status=$? echo "Inside catch_err" cleanexit $exit_status } # catch_err() #--- catch_sig() -------------------------------------------------------------- # Catch signal trap. # Trap not-normal exit signals: 1/HUP, 2/INT, 3/QUIT, 15/TERM # @NOTE1: Non-trapped signals are 0/EXIT, 9/KILL. function catch_sig() { local exit_status=$? echo "Inside catch_sig" cleanexit $exit_status } # catch_sig() # An erroneous command should have exit status 127. # The erroneous command should be trapped by the ERR trap. #erroneous_command # A command returning false should have exit status <> 0 # The false returning command should be trapped by the ERR trap. #false # Manually calling 'cleanexit' #cleanexit # Manually calling 'cleanexit' with exit status #cleanexit 5 # Killing a process via CTRL-C is handled via the SIGINT trap #sleep 20 # A syntax error is not trapped, but should have exit status 2 #fi # Allways call 'cleanexit' at end of script cleanexit