Sagar.BlogArticle
All posts
All posts
Bash

Standard Error, Logging, and Output Redirection

Error messages belong on stderr, not stdout. Learn to write robust error functions, separate diagnostic output from data output, and implement simple logging.

January 30, 20266 min read
BashStderrLoggingIOScripting

Unix programs have two output streams: stdout (file descriptor 1) for data and results, and stderr (file descriptor 2) for errors, warnings, and diagnostic messages. Mixing them breaks pipelines and automated processing. Getting this right separates beginner scripts from professional ones.

Writing to stderr

# Redirect echo to stderr
echo "Error: file not found" >&2

# Preferred: redirect entire block
{
    echo "Error: configuration is invalid"
    echo "Check: $config_file"
} >&2

# printf to stderr
printf "Error: expected %d args, got %d\n" 2 "$#" >&2

# Function pattern
function die {
    echo "$*" >&2
    exit 1
}

die "Fatal: database connection failed"

Redirection Reference

Common redirection operators

SyntaxMeaning
> fileRedirect stdout to file (create/truncate)
>> fileRedirect stdout to file (append)
2> fileRedirect stderr to file
2>&1Redirect stderr to stdout (merge)
&> fileRedirect both stdout and stderr to file
&>> fileAppend both to file
2>/dev/nullDiscard stderr (silence errors)
>/dev/null 2>&1Discard everything
1>&2Redirect stdout to stderr

Building a Logger

#!/usr/bin/env bash

# Log level: DEBUG=0, INFO=1, WARN=2, ERROR=3
LOG_LEVEL=${LOG_LEVEL:-1}
LOG_FILE=${LOG_FILE:-""}

function _log {
    local level="$1"
    local level_num="$2"
    shift 2
    local message="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    local line="[$timestamp] [$level] $message"

    if [[ $level_num -ge $LOG_LEVEL ]]; then
        if [[ $level_num -ge 2 ]]; then
            echo "$line" >&2     # WARN and ERROR → stderr
        else
            echo "$line"         # DEBUG and INFO → stdout
        fi
    fi

    if [[ -n "$LOG_FILE" ]]; then
        echo "$line" >> "$LOG_FILE"
    fi
}

function log_debug { _log "DEBUG" 0 "$@"; }
function log_info  { _log "INFO " 1 "$@"; }
function log_warn  { _log "WARN " 2 "$@"; }
function log_error { _log "ERROR" 3 "$@"; }

# Usage
log_info  "Script started"
log_warn  "Disk usage at 85%"
log_error "Failed to connect to DB"

Capturing stderr and stdout Separately

# Capture stdout, discard stderr
output=$(cmd 2>/dev/null)

# Capture stdout, let stderr through to terminal
output=$(cmd)

# Capture both in separate variables (Bash 4+ trick)
exec 3>&1                          # save stdout to fd 3
output=$(cmd 2>&1 1>&3)            # stderr captured, stdout goes to fd3 (terminal)
exec 3>&-                          # close fd 3
# Now: $output has stderr, terminal saw stdout

# Simple: capture both in one variable
combined=$(cmd 2>&1)

# Capture stdout to file, stderr to terminal
cmd > output.txt

# Capture stderr to file, stdout to terminal
cmd 2> errors.txt
Quick Check

What does `command > output.txt 2>&1` do?

Exercise

Write a script run-with-log.sh that:

  1. Takes a command and its args as arguments (use "$@")
  2. Runs the command
  3. Logs stdout to /tmp/run.stdout.log
  4. Logs stderr to /tmp/run.stderr.log
  5. Also shows both streams in the terminal in real time (use tee)
  6. Prints the exit code at the end