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
| Syntax | Meaning |
|---|---|
> file | Redirect stdout to file (create/truncate) |
>> file | Redirect stdout to file (append) |
2> file | Redirect stderr to file |
2>&1 | Redirect stderr to stdout (merge) |
&> file | Redirect both stdout and stderr to file |
&>> file | Append both to file |
2>/dev/null | Discard stderr (silence errors) |
>/dev/null 2>&1 | Discard everything |
1>&2 | Redirect 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.txtQuick Check
What does `command > output.txt 2>&1` do?
Exercise
Write a script run-with-log.sh that:
- Takes a command and its args as arguments (use
"$@") - Runs the command
- Logs stdout to
/tmp/run.stdout.log - Logs stderr to
/tmp/run.stderr.log - Also shows both streams in the terminal in real time (use
tee) - Prints the exit code at the end