# Shell Scripting for DevOps Engineers — Error Handling & Debugging

> **A good Shell script should not only work when everything goes right — it should also detect errors, show useful information, and help us troubleshoot failures.**

In the previous articles, we covered **loops, conditions, functions, arrays, and file handling**.

In this article, we will learn **Error Handling & Debugging in Shell Scripting**, which is especially important when writing scripts for **DevOps automation and CI/CD pipelines**.

* * *

## 1\. What is Error Handling?

Error handling means detecting when something goes wrong and taking an appropriate action.

For example:

```text
Run Command
     ↓
Command Successful?
   ↙       ↘
 Yes        No
 ↓           ↓
Continue    Handle Error
```

Example:

```bash
#!/bin/bash

if systemctl is-active --quiet nginx
then
    echo "Nginx is running"
else
    echo "ERROR: Nginx is not running"
fi
```

* * *

## 2\. What is Debugging?

**Debugging** means finding and fixing problems in a script.

For example, if your script produces:

```text
ERROR: Variable is empty
```

you investigate:

*   Which variable is empty?
    
*   Where was it assigned?
    
*   Which command failed?
    
*   What value was returned?
    
*   Which line caused the problem?
    

* * *

## 3\. Exit Status

Every Linux command returns an exit status.

```text
0     → Success
Non-0 → Failure
```

Example:

```bash
ls /tmp
echo $?
```

If the command succeeds:

```text
0
```

* * *

## 4\. Using `$?`

`$?` contains the exit status of the previous command.

```bash
#!/bin/bash

mkdir test-directory

echo "Exit status: $?"
```

If the directory is created successfully:

```text
Exit status: 0
```

* * *

## 5\. Handling Command Failure

You can check the result with `if`:

```bash
#!/bin/bash

if mkdir test-directory
then
    echo "Directory created successfully"
else
    echo "Failed to create directory"
    exit 1
fi
```

This is better than blindly continuing after a failure.

* * *

## 6\. Using `exit`

The `exit` command terminates the script.

```bash
#!/bin/bash

echo "Starting deployment..."

if [ ! -f "application.jar" ]
then
    echo "ERROR: application.jar not found"
    exit 1
fi

echo "Application found"
```

Here, `exit 1` stops the script when the required file is missing.

* * *

## 7\. `exit 0` and `exit 1`

Generally:

```bash
exit 0
```

means successful completion.

```bash
exit 1
```

means failure.

Example:

```bash
#!/bin/bash

echo "Deployment completed successfully"

exit 0
```

* * *

## 8\. `return` vs `exit`

This is an important interview question.

### `return`

Used to return from a function:

```bash
check_status() {
    return 1
}
```

### `exit`

Stops the entire script:

```bash
exit 1
```

Simple difference:

```text
return → Exit function
exit   → Exit script
```

* * *

## 9\. `set -e`

`set -e` tells Bash to exit when a command fails in contexts where that failure is not explicitly handled.

```bash
#!/bin/bash

set -e

echo "Step 1"

ls /invalid-directory

echo "Step 2"
```

Because the `ls` command fails, the script exits instead of continuing normally.

* * *

## 10\. `set -u`

`set -u` treats unset variables as errors in many normal parameter-expansion contexts.

```bash
#!/bin/bash

set -u

echo "$USERNAME"
```

If `USERNAME` has not been defined, Bash reports an error.

This helps catch spelling mistakes and missing variables.

* * *

## 11\. `set -o pipefail`

Consider:

```bash
command1 | command2
```

Without `pipefail`, the pipeline's status is normally determined by the last command.

With:

```bash
set -o pipefail
```

a failure in an earlier pipeline command can cause the pipeline to return a failure status.

Example:

```bash
#!/bin/bash

set -o pipefail

false | true

echo $?
```

The pipeline returns a non-zero status because `false` failed.

* * *

## 12\. `set -euo pipefail`

A common Bash safety pattern is:

```bash
#!/bin/bash

set -euo pipefail
```

Meaning:

```text
-e → Exit on unhandled command failure
-u → Treat unset variables as errors
pipefail → Detect failures in pipelines
```

These options have Bash-specific edge cases, so scripts should still be tested carefully.

* * *

13\. Debugging with `bash -x`

One of the easiest ways to debug a Shell script is:

```bash
bash -x script.sh
```

Example:

```bash
bash -x deploy.sh
```

Bash prints commands as they are executed.

This helps you understand the script's execution flow.

* * *

## 14\. Debugging with `set -x`

You can also enable debugging inside the script:

```bash
#!/bin/bash

set -x

echo "Starting script"

NAME="DevOps"

echo "Hello $NAME"

set +x
```

Here:

```text
set -x  → Enable debugging
set +x  → Disable debugging
```

* * *

## 15\. Debugging Only a Specific Section

You don't always need debugging for the entire script.

```bash
#!/bin/bash

echo "Starting application"

set -x

docker build -t myapp .

set +x

echo "Build completed"
```

This keeps debug output focused on the section you're troubleshooting.

* * *

## 16\. Printing Variables for Debugging

Suppose:

```bash
ENV="production"
SERVER="server01"
```

You can temporarily print values:

```bash
echo "ENV=$ENV"
echo "SERVER=$SERVER"
```

This helps identify unexpected or empty values.

**Be careful:** never print passwords, tokens, private keys, or other secrets into logs.

* * *

## 17\. Debugging with `echo`

A simple technique is to add messages between steps:

```bash
echo "Step 1: Starting build"
./build.sh

echo "Step 2: Starting tests"
./test.sh

echo "Step 3: Starting deployment"
./deploy.sh
```

If the script stops, you can quickly identify which stage was reached.

* * *

## 18\. Error Output with `>&2`

Error messages can be sent to standard error:

```bash
echo "ERROR: File not found" >&2
```

Linux commonly uses:

```text
0 → stdin
1 → stdout
2 → stderr
```

This is useful for separating normal output from errors.

* * *

## 19\. Logging Errors

You can save errors into a log file:

```bash
#!/bin/bash

./deploy.sh 2>> deployment-error.log
```

Now errors are appended to:

```text
deployment-error.log
```

* * *

## 20\. Redirect Output and Errors

You can redirect both standard output and standard error:

```bash
./deploy.sh > deployment.log 2>&1
```

Or, in Bash:

```bash
./deploy.sh &> deployment.log
```

This is useful when troubleshooting automated jobs.

* * *

## 21\. Using `trap`

`trap` allows you to execute commands when certain signals or exit events occur.

Example:

```bash
#!/bin/bash

cleanup() {
    echo "Cleaning temporary files..."
}

trap cleanup EXIT

echo "Running script..."
```

When the script exits, the cleanup function runs.

* * *

## 22\. Error Handling with `trap`

A useful pattern is to capture the failing line:

```bash
#!/bin/bash

trap 'echo "Error on line $LINENO"' ERR

set -E

echo "Starting script"

ls /invalid-directory

echo "This may not execute"
```

`trap ... ERR` can be useful for troubleshooting failures.

For more complex scripts, test the behavior carefully because Bash error traps have specific rules around functions, subshells, and conditional commands.

* * *

## 23\. Real-Time DevOps Example — Docker Build

```bash
#!/bin/bash

set -euo pipefail

IMAGE="myapp:latest"

echo "Building Docker image..."

if docker build -t "$IMAGE" .
then
    echo "Docker image built successfully"
else
    echo "ERROR: Docker build failed" >&2
    exit 1
fi
```

This prevents the script from continuing after a failed Docker build.

* * *

## 24\. Real-Time DevOps Example — Kubernetes

```bash
#!/bin/bash

set -euo pipefail

NAMESPACE="dev"

echo "Checking Kubernetes pods..."

if kubectl get pods -n "$NAMESPACE"
then
    echo "Kubernetes check successful"
else
    echo "ERROR: Kubernetes check failed" >&2
    exit 1
fi
```

* * *

## 25\. Real-Time DevOps Example — AWS CLI

```bash
#!/bin/bash

set -euo pipefail

echo "Checking AWS authentication..."

if aws sts get-caller-identity
then
    echo "AWS authentication successful"
else
    echo "ERROR: AWS authentication failed" >&2
    exit 1
fi
```

* * *

## 26\. Real-Time CI/CD Example

Imagine Jenkins executes:

```bash
./deploy.sh
```

The script performs:

```text
Build
  ↓
Test
  ↓
Docker Build
  ↓
Docker Push
  ↓
Kubernetes Deployment
```

If Docker build fails:

```text
Docker Build
     ↓
Failure
     ↓
exit 1
     ↓
Jenkins receives failure status
     ↓
Pipeline step fails
```

This is why proper exit codes and error handling are important in CI/CD.

* * *

## 27\. Complete DevOps Error Handling Example

```bash
#!/bin/bash

set -euo pipefail

APP="myapp"
IMAGE="$APP:latest"

echo "================================"
echo "Starting Deployment"
echo "================================"

echo "Step 1: Checking Docker..."

if ! docker info >/dev/null 2>&1
then
    echo "ERROR: Docker is not available" >&2
    exit 1
fi

echo "Docker is available"

echo "Step 2: Building image..."

docker build -t "$IMAGE" .

echo "Docker build successful"

echo "Step 3: Running container..."

docker run -d --name "$APP" "$IMAGE"

echo "Container started successfully"

echo "================================"
echo "Deployment Completed"
echo "================================"
```

This script demonstrates:

```text
set -euo pipefail
       ↓
Prerequisite check
       ↓
Docker build
       ↓
Container deployment
       ↓
Success / Failure
```

* * *

## 28\. Common Debugging Problems

### Problem 1: Variable is empty

```bash
echo "$ENV"
```

Check whether the variable was properly assigned.

* * *

### Problem 2: Permission denied

Check:

```bash
ls -l script.sh
```

Give execute permission if appropriate:

```bash
chmod +x script.sh
```

* * *

### Problem 3: Command not found

Check:

```bash
command -v docker
```

or:

```bash
command -v kubectl
```

* * *

### Problem 4: Script works manually but fails in Jenkins

Check:

*   Environment variables
    
*   PATH
    
*   File permissions
    
*   Working directory
    
*   Credentials
    
*   User permissions
    
*   Shell/interpreter
    
*   Exit status
    
*   Tool availability
    

* * *

## 29\. Best Practices

✅ Use meaningful error messages.

✅ Check important command results.

✅ Use appropriate exit codes.

✅ Use `set -euo pipefail` thoughtfully.

✅ Use `bash -x` when debugging.

✅ Validate required variables.

✅ Validate files before processing them.

✅ Log useful information.

✅ Send errors to `stderr`.

✅ Never expose secrets in logs.

✅ Test scripts before using them in production.

* * *

# 🎯 Key Takeaways

```text
Error Handling
      ↓
Exit Status
      ↓
$?
      ↓
if / else
      ↓
exit / return
      ↓
set -euo pipefail
      ↓
Debugging
      ↓
bash -x
      ↓
Logging
      ↓
trap
      ↓
Reliable DevOps Automation
```

You should now understand:

✅ Exit status ✅ `$?` ✅ `exit` ✅ `return` ✅ `set -e` ✅ `set -u` ✅ `pipefail` ✅ `set -x` ✅ `bash -x` ✅ `trap` ✅ Error logging ✅ Docker error handling ✅ Kubernetes error handling ✅ AWS CLI error handling ✅ CI/CD error handling

> **Error handling makes scripts safer, while debugging helps us quickly identify and fix problems. Together, they are essential for reliable Shell-based DevOps automation.**

![](https://cdn.hashnode.com/uploads/covers/6878a472fb2990c4c07d4019/7eef9da8-344c-4086-81e0-71bf2b391932.png align="center")
