[linux] Raise error in a Bash script

I want to raise an error in a Bash script with message "Test cases Failed !!!". How to do this in Bash?

For example:

if [ condition ]; then
    raise error "Test cases failed !!!"
fi

This question is related to linux bash shell error-handling scripting

The answer is


There are a couple more ways with which you can approach this problem. Assuming one of your requirement is to run a shell script/function containing a few shell commands and check if the script ran successfully and throw errors in case of failures.

The shell commands in generally rely on exit-codes returned to let the shell know if it was successful or failed due to some unexpected events.

So what you want to do falls upon these two categories

  • exit on error
  • exit and clean-up on error

Depending on which one you want to do, there are shell options available to use. For the first case, the shell provides an option with set -e and for the second you could do a trap on EXIT

Should I use exit in my script/function?

Using exit generally enhances readability In certain routines, once you know the answer, you want to exit to the calling routine immediately. If the routine is defined in such a way that it doesn’t require any further cleanup once it detects an error, not exiting immediately means that you have to write more code.

So in cases if you need to do clean-up actions on script to make the termination of the script clean, it is preferred to not to use exit.

Should I use set -e for error on exit?

No!

set -e was an attempt to add "automatic error detection" to the shell. Its goal was to cause the shell to abort any time an error occurred, but it comes with a lot of potential pitfalls for example,

  • The commands that are part of an if test are immune. In the example, if you expect it to break on the test check on the non-existing directory, it wouldn't, it goes through to the else condition

    set -e
    f() { test -d nosuchdir && echo no dir; }
    f
    echo survived
    
  • Commands in a pipeline other than the last one, are immune. In the example below, because the most recently executed (rightmost) command's exit code is considered ( cat) and it was successful. This could be avoided by setting by the set -o pipefail option but its still a caveat.

    set -e
    somecommand that fails | cat -
    echo survived 
    

Recommended for use - trap on exit

The verdict is if you want to be able to handle an error instead of blindly exiting, instead of using set -e, use a trap on the ERR pseudo signal.

The ERR trap is not to run code when the shell itself exits with a non-zero error code, but when any command run by that shell that is not part of a condition (like in if cmd, or cmd ||) exits with a non-zero exit status.

The general practice is we define an trap handler to provide additional debug information on which line and what cause the exit. Remember the exit code of the last command that caused the ERR signal would still be available at this point.

cleanup() {
    exitcode=$?
    printf 'error condition hit\n' 1>&2
    printf 'exit code returned: %s\n' "$exitcode"
    printf 'the command executing at the time of the error was: %s\n' "$BASH_COMMAND"
    printf 'command present on line: %d' "${BASH_LINENO[0]}"
    # Some more clean up code can be added here before exiting
    exit $exitcode
}

and we just use this handler as below on top of the script that is failing

trap cleanup ERR

Putting this together on a simple script that contained false on line 15, the information you would be getting as

error condition hit
exit code returned: 1
the command executing at the time of the error was: false
command present on line: 15

The trap also provides options irrespective of the error to just run the cleanup on shell completion (e.g. your shell script exits), on signal EXIT. You could also trap on multiple signals at the same time. The list of supported signals to trap on can be found on the trap.1p - Linux manual page

Another thing to notice would be to understand that none of the provided methods work if you are dealing with sub-shells are involved in which case, you might need to add your own error handling.

  • On a sub-shell with set -e wouldn't work. The false is restricted to the sub-shell and never gets propagated to the parent shell. To do the error handling here, add your own logic to do (false) || false

    set -e
    (false)
    echo survived
    
  • The same happens with trap also. The logic below wouldn't work for the reasons mentioned above.

    trap 'echo error' ERR
    (false)
    

I often find it useful to write a function to handle error messages so the code is cleaner overall.

# Usage: die [exit_code] [error message]
die() {
  local code=$? now=$(date +%T.%N)
  if [ "$1" -ge 0 ] 2>/dev/null; then  # assume $1 is an error code if numeric
    code="$1"
    shift
  fi
  echo "$0: ERROR at ${now%???}${1:+: $*}" >&2
  exit $code
}

This takes the error code from the previous command and uses it as the default error code when exiting the whole script. It also notes the time, with microseconds where supported (GNU date's %N is nanoseconds, which we truncate to microseconds later).

If the first option is zero or a positive integer, it becomes the exit code and we remove it from the list of options. We then report the message to standard error, with the name of the script, the word "ERROR", and the time (we use parameter expansion to truncate nanoseconds to microseconds, or for non-GNU times, to truncate e.g. 12:34:56.%N to 12:34:56). A colon and space are added after the word ERROR, but only when there is a provided error message. Finally, we exit the script using the previously determined exit code, triggering any traps as normal.

Some examples (assume the code lives in script.sh):

if [ condition ]; then die 123 "condition not met"; fi
# exit code 123, message "script.sh: ERROR at 14:58:01.234564: condition not met"

$command |grep -q condition || die 1 "'$command' lacked 'condition'"
# exit code 1, "script.sh: ERROR at 14:58:55.825626: 'foo' lacked 'condition'"

$command || die
# exit code comes from command's, message "script.sh: ERROR at 14:59:15.575089"

Basic error handling

If your test case runner returns a non-zero code for failed tests, you can simply write:

test_handler test_case_x; test_result=$?
if ((test_result != 0)); then
  printf '%s\n' "Test case x failed" >&2  # write error message to stderr
  exit 1                                  # or exit $test_result
fi

Or even shorter:

if ! test_handler test_case_x; then
  printf '%s\n' "Test case x failed" >&2
  exit 1
fi

Or the shortest:

test_handler test_case_x || { printf '%s\n' "Test case x failed" >&2; exit 1; }

To exit with test_handler's exit code:

test_handler test_case_x || { ec=$?; printf '%s\n' "Test case x failed" >&2; exit $ec; }

Advanced error handling

If you want to take a more comprehensive approach, you can have an error handler:

exit_if_error() {
  local exit_code=$1
  shift
  [[ $exit_code ]] &&               # do nothing if no error code passed
    ((exit_code != 0)) && {         # do nothing if error code is 0
      printf 'ERROR: %s\n' "$@" >&2 # we can use better logging here
      exit "$exit_code"             # we could also check to make sure
                                    # error code is numeric when passed
    }
}

then invoke it after running your test case:

run_test_case test_case_x
exit_if_error $? "Test case x failed"

or

run_test_case test_case_x || exit_if_error $? "Test case x failed"

The advantages of having an error handler like exit_if_error are:

  • we can standardize all the error handling logic such as logging, printing a stack trace, notification, doing cleanup etc., in one place
  • by making the error handler get the error code as an argument, we can spare the caller from the clutter of if blocks that test exit codes for errors
  • if we have a signal handler (using trap), we can invoke the error handler from there

Error handling and logging library

Here is a complete implementation of error handling and logging:

https://github.com/codeforester/base/blob/master/lib/stdlib.sh


Related posts


Here's a simple trap that prints the last argument of whatever failed to STDERR, reports the line it failed on, and exits the script with the line number as the exit code. Note these are not always great ideas, but this demonstrates some creative application you could build on.

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR

I put that in a script with a loop to test it. I just check for a hit on some random numbers; you might use actual tests. If I need to bail, I call false (which triggers the trap) with the message I want to throw.

For elaborated functionality, have the trap call a processing function. You can always use a case statement on your arg ($_) if you need to do more cleanup, etc. Assign to a var for a little syntactic sugar -

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR
throw=false
raise=false

while :
do x=$(( $RANDOM % 10 ))
   case "$x" in
   0) $throw "DIVISION BY ZERO" ;;
   3) $raise "MAGIC NUMBER"     ;;
   *) echo got $x               ;;
   esac
done

Sample output:

# bash tst
got 2
got 8
DIVISION BY ZERO at 6
# echo $?
6

Obviously, you could

runTest1 "Test1 fails" # message not used if it succeeds

Lots of room for design improvement.

The draw backs include the fact that false isn't pretty (thus the sugar), and other things tripping the trap might look a little stupid. Still, I like this method.


You have 2 options: Redirect the output of the script to a file, Introduce a log file in the script and

  1. Redirecting output to a file:

Here you assume that the script outputs all necessary info, including warning and error messages. You can then redirect the output to a file of your choice.

./runTests &> output.log

The above command redirects both the standard output and the error output to your log file.

Using this approach you don't have to introduce a log file in the script, and so the logic is a tiny bit easier.

  1. Introduce a log file to the script:

In your script add a log file either by hard coding it:

logFile='./path/to/log/file.log'

or passing it by a parameter:

logFile="${1}"  # This assumes the first parameter to the script is the log file

It's a good idea to add the timestamp at the time of execution to the log file at the top of the script:

date '+%Y%-m%d-%H%M%S' >> "${logFile}"

You can then redirect your error messages to the log file

if [ condition ]; then
    echo "Test cases failed!!" >> "${logFile}"; 
fi

This will append the error to the log file and continue execution. If you want to stop execution when critical errors occur, you can exit the script:

if [ condition ]; then
    echo "Test cases failed!!" >> "${logFile}"; 
    # Clean up if needed
    exit 1;
fi

Note that exit 1 indicates that the program stop execution due to an unspecified error. You can customize this if you like.

Using this approach you can customize your logs and have a different log file for each component of your script.


If you have a relatively small script or want to execute somebody else's script without modifying it to the first approach is more suitable.

If you always want the log file to be at the same location, this is the better option of the 2. Also if you have created a big script with multiple components then you may want to log each part differently and the second approach is your only option.


Examples related to linux

grep's at sign caught as whitespace How to prevent Google Colab from disconnecting? "E: Unable to locate package python-pip" on Ubuntu 18.04 How to upgrade Python version to 3.7? Install Qt on Ubuntu Get first line of a shell command's output Cannot connect to the Docker daemon at unix:/var/run/docker.sock. Is the docker daemon running? Run bash command on jenkins pipeline How to uninstall an older PHP version from centOS7 How to update-alternatives to Python 3 without breaking apt?

Examples related to bash

Comparing a variable with a string python not working when redirecting from bash script Zipping a file in bash fails How do I prevent Conda from activating the base environment by default? Get first line of a shell command's output Fixing a systemd service 203/EXEC failure (no such file or directory) /bin/sh: apt-get: not found VSCode Change Default Terminal Run bash command on jenkins pipeline How to check if the docker engine and a docker container are running? How to switch Python versions in Terminal?

Examples related to shell

Comparing a variable with a string python not working when redirecting from bash script Get first line of a shell command's output How to run shell script file using nodejs? Run bash command on jenkins pipeline Way to create multiline comments in Bash? How to do multiline shell script in Ansible How to check if a file exists in a shell script How to check if an environment variable exists and get its value? Curl to return http status code along with the response docker entrypoint running bash script gets "permission denied"

Examples related to error-handling

must declare a named package eclipse because this compilation unit is associated to the named module Error:Failed to open zip file. Gradle's dependency cache may be corrupt What does 'index 0 is out of bounds for axis 0 with size 0' mean? What's the source of Error: getaddrinfo EAI_AGAIN? Error handling with try and catch in Laravel What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean? Raise error in a Bash script Javascript Uncaught TypeError: Cannot read property '0' of undefined Multiple values in single-value context IndexError: too many indices for array

Examples related to scripting

What does `set -x` do? Creating an array from a text file in Bash Windows batch - concatenate multiple text files into one Raise error in a Bash script How do I assign a null value to a variable in PowerShell? Difference between ${} and $() in Bash Using a batch to copy from network drive to C: or D: drive Check if a string matches a regex in Bash script How to run a script at a certain time on Linux? How to make an "alias" for a long path?