How to Echo Output to stderr in Bash
TLDR
To send output to stderr instead of stdout in Bash, redirect to file descriptor 2 using >&2. The most common approach is echo "error message" >&2. This separates error messages from normal output, which is important for scripts that other programs consume or when you want to redirect output separately.
Understanding stdout and stderr
Unix programs have three standard streams:
- stdin (0): Standard input
- stdout (1): Standard output
- stderr (2): Standard error
By default, echo and most commands write to stdout (file descriptor 1). Error messages should go to stderr (file descriptor 2) so they can be handled separately from regular output.
Basic Method: Redirect to &2
The simplest way to echo to stderr is using output redirection:
#!/bin/bash
echo "This goes to stdout"
echo "This goes to stderr" >&2
The >&2 syntax redirects the output to file descriptor 2 (stderr). When you run this script:
./script.sh
Both lines appear in your terminal because your terminal displays both stdout and stderr. But you can redirect them separately:
./script.sh 2>/dev/null # Hide stderr, show stdout
./script.sh 1>/dev/null # Hide stdout, show stderr
./script.sh > output.txt 2> errors.txt # Separate files
Why It Matters
Separating stdout and stderr enables clean pipelines and proper error handling:
# Extract data from a script
result=$(./process-data.sh 2>/dev/null)
# If process-data.sh writes errors to stderr,
# they don't pollute the $result variable
The flow looks like:
Script output
|
+---> stdout (data, results)
| |
| +---> Can be piped to other commands
| +---> Captured in variables
| +---> Redirected to files
|
+---> stderr (errors, warnings, logs)
|
+---> Displayed to user
+---> Logged to error files
+---> Monitored by supervisors
Creating Helper Functions
For cleaner code, define helper functions for error output:
#!/bin/bash
error() {
echo "$@" >&2
}
warn() {
echo "WARNING: $@" >&2
}
die() {
echo "FATAL: $@" >&2
exit 1
}
# Usage examples
warn "Configuration file not found, using defaults"
error "Database connection failed"
die "Cannot continue without database access"
These functions make your intent explicit and your code more readable. The die function is particularly useful for fatal errors that should stop script execution.
Practical Example: File Processing Script
Here's a script that processes files and properly separates output from errors:
#!/bin/bash
set -euo pipefail
process_file() {
local file="$1"
if [ ! -f "$file" ]; then
echo "Error: File not found: $file" >&2
return 1
fi
if [ ! -r "$file" ]; then
echo "Error: Cannot read file: $file" >&2
return 1
fi
# Process the file and output results to stdout
local line_count=$(wc -l < "$file")
echo "$file:$line_count"
}
# Main script
if [ $# -eq 0 ]; then
echo "Usage: $0 <file1> [file2] [file3] ..." >&2
exit 1
fi
echo "Processing files..." >&2
for file in "$@"; do
process_file "$file"
done
echo "Processing complete" >&2
When you run this script:
./process.sh file1.txt file2.txt file3.txt > results.txt
The results go to results.txt while status messages appear in the terminal. If a file is missing, the error message displays without contaminating your output file.
Using printf for Formatted Output
For more control over formatting, use printf instead of echo:
#!/bin/bash
printf "Error: %s\n" "Something went wrong" >&2
printf "Failed to process file: %s (error code: %d)\n" "$filename" "$error_code" >&2
The printf command gives you format specifiers like %s for strings and %d for integers, making error messages more consistent.
Logging with Timestamps
For production scripts, adding timestamps to error messages helps with debugging:
#!/bin/bash
log_error() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
printf "[%s] ERROR: %s\n" "$timestamp" "$*" >&2
}
log_warn() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
printf "[%s] WARN: %s\n" "$timestamp" "$*" >&2
}
log_info() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
printf "[%s] INFO: %s\n" "$timestamp" "$*" >&2
}
# Usage
log_info "Starting backup process"
log_warn "Disk space is running low"
log_error "Backup failed: connection timeout"
This creates a consistent log format that's easy to parse and search.
Redirecting Both stdout and stderr
Sometimes you want to send both streams to the same destination:
# Redirect both stdout and stderr to a file
./script.sh > output.log 2>&1
# Redirect both to /dev/null (suppress all output)
./script.sh > /dev/null 2>&1
# Or the shorter Bash 4+ syntax
./script.sh &> output.log
./script.sh &> /dev/null
The 2>&1 syntax means "redirect stderr (2) to wherever stdout (1) is currently going". Order matters:
# CORRECT: Redirect stdout to file, then stderr to stdout's destination
./script.sh > output.log 2>&1
# WRONG: Redirect stderr to current stdout (terminal), then change stdout
./script.sh 2>&1 > output.log # stderr still goes to terminal!
Colored Output for Errors
For interactive scripts, color-coding error messages makes them stand out:
#!/bin/bash
# ANSI color codes
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
error() {
printf "${RED}Error: %s${NC}\n" "$*" >&2
}
warn() {
printf "${YELLOW}Warning: %s${NC}\n" "$*" >&2
}
# Usage
error "Database connection failed"
warn "API rate limit approaching"
The colors only work when outputting to a terminal. For better compatibility, detect if stderr is a terminal:
#!/bin/bash
if [ -t 2 ]; then
# stderr is a terminal, use colors
RED='\033[0;31m'
NC='\033[0m'
else
# stderr is redirected, no colors
RED=''
NC=''
fi
error() {
printf "${RED}Error: %s${NC}\n" "$*" >&2
}
The [ -t 2 ] test checks if file descriptor 2 (stderr) is connected to a terminal.
Combining with Exit Codes
Error messages to stderr should accompany appropriate exit codes:
#!/bin/bash
validate_input() {
local port="$1"
if ! [[ "$port" =~ ^[0-9]+$ ]]; then
echo "Error: Port must be a number" >&2
return 1
fi
if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
echo "Error: Port must be between 1 and 65535" >&2
return 1
fi
return 0
}
# Main script
if [ $# -eq 0 ]; then
echo "Usage: $0 <port>" >&2
exit 1
fi
if ! validate_input "$1"; then
exit 1
fi
echo "Starting server on port $1"
Non-zero exit codes signal failure to calling processes, while stderr provides human-readable context.
Best Practices
When writing scripts that others will use or that run in production:
1. Always use stderr for errors and diagnostics:
# Good
echo "Error: Configuration invalid" >&2
# Bad
echo "Error: Configuration invalid" # Goes to stdout
2. Keep stdout clean for data:
# Good - data goes to stdout, messages to stderr
echo "Processing..." >&2
find /data -name "*.log" | process_data
echo "Complete" >&2
# Bad - messages mixed with data
echo "Processing..."
find /data -name "*.log" | process_data
echo "Complete"
3. Use consistent error prefixes:
echo "ERROR: $message" >&2
echo "WARNING: $message" >&2
echo "INFO: $message" >&2
4. Consider verbosity levels:
#!/bin/bash
VERBOSE=${VERBOSE:-0}
debug() {
if [ "$VERBOSE" -ge 2 ]; then
echo "DEBUG: $*" >&2
fi
}
info() {
if [ "$VERBOSE" -ge 1 ]; then
echo "INFO: $*" >&2
fi
}
error() {
echo "ERROR: $*" >&2
}
# Usage: VERBOSE=2 ./script.sh
This allows users to control output detail without changing the script.
Writing to stderr is a small detail that significantly improves script quality. It makes your scripts composable in pipelines, allows users to separate output from diagnostics, and follows Unix conventions that other developers expect. Whether you're writing a quick automation script or a production deployment tool, properly using stderr demonstrates attention to detail and makes your scripts more professional.
Found an issue?