Don't Use stderr to Determine Process Failure Because Logs Default to stderr
It's a beautiful day, and it started with a simple code review:
1# tools/foo/main.go
2- fmt.Println("found it")
3+ log.Println("found it")
The author explained the advantages of using a logging library over plain printf. The rationale was straightforward, so I approved the change without hesitation. However, two hours later, another code review came through—this time reverting the change because the load test pipeline had failed. Intriguing. Let's take a closer look.
foo
is used by bar
:
1// tools/bar/main.go
2func main() {
3 cmdName := os.Args[1]
4 cmd := exec.CommandContext(context.Background(), cmdName)
5 var stdout, stderr bytes.Buffer
6 cmd.Stdout = &stdout
7 cmd.Stderr = &stderr
8 if err := cmd.Run(); err != nil {
9 fmt.Printf("exec error, %v, %v\n", cmdName, err)
10 os.Exit(1)
11 }
12
13 if s := stderr.String(); s != "" {
14 fmt.Printf("exec error, %v, %v\n", cmdName, s)
15 os.Exit(1)
16 }
17}
The bar
program calls foo
successfully if foo exits with a status code of 0 and its standard error is empty. The issue lies in this line:
if s := stderr.String(); s != "" {
. Here, bar checks the standard error of foo. However, Go's standard log library writes logs to stderr by default.
This causes bar to interpret foo's log output as an error, even though foo executed successfully.
As a result, bar terminates with a non-zero exit code, mistakenly thinking something went wrong.
Logs going to stderr may sound surprising at first—after all, there is "err" in stderr and logs are not necessarily errors. But it actually makes sense. A program's stdout should communicate its output back to the caller, not internal implementation details like logs. This separation helps when using stdout for Inter Process Communication (IPC), which is common.
For simplicity, this blog focuses on the default behaviors of logging libraries. Note that all languages and libraries mentioned here support configuring the logging destination.
What about other logging libraries in Go?
Although the standard log package allows customizing the default output using log.SetOutput, I was surprised by the default behavior. I expected logs with levels lower than ERROR to go to stdout, and only ERROR or higher to go to stderr. Curious about this behavior, I examined a few popular Go logging libraries: zap, logrus, and zerolog. All three libraries also log to stderr by default, aligning with the standard library.
1package main
2
3import (
4 "go.uber.org/zap"
5 "github.com/sirupsen/logrus"
6 zerolog "github.com/rs/zerolog/log"
7)
8
9func main() {
10 logger, err := zap.NewProduction()
11 if err != nil {
12 log.Fatal(err)
13 }
14 defer func() {
15 _ = logger.Sync()
16 }()
17 zap.ReplaceGlobals(logger)
18 zap.S().Info("This is zap.S().Info().")
19
20 logrus.Info("This is logrus.Info().")
21 zerolog.Info().Msg("This is zerolog.Info().")
22}
1$ go build -o foo ./foo/main.go
2$ ./foo > stdout.log 2> stderr.log
3$ cat stderr.log
4{"level":"info","ts":1733023209.4200065,"caller":"foo/main.go:19","msg":"This is zap.S().Info()."}
5INFO[0000] This is logrus.Info().
6{"level":"info","time":"2024-12-01T03:20:09Z","message":"This is zerolog.Info()."}
What about other languages?
At this point, I realized that logging to stderr is both intentional and conventional. The philosophy seems to be reserving stdout exclusively for program output, while using stderr for diagnostic or auxiliary output. But how widely does this convention apply across other languages?
Rust
In Rust, the standard log crate provides macros like info!
and warn!
, but
delegates actual log processing to implementations. One popular implementation, env_logger,
logs to stderr by default.
1use log::{info};
2
3fn main() {
4 env_logger::init();
5 info!("This is info!().");
6}
1$ RUST_LOG=info ./target/release/rust-logging > stdout.log 2> stderr.log
2$ cat stdout.log
3$ cat stderr.log
4[2024-12-01T04:55:51Z INFO rust_logging] This is info!().
Java
In Java, the standard java.util.logging package logs to stderr by default.
1import java.util.logging.Logger;
2
3public class Main {
4 private static final Logger logger = Logger.getLogger(Main.class.getName());
5
6 public static void main(String[] args) {
7 logger.info("This is logger.info().");
8 logger.warning("This is logger.warning().");
9 logger.severe("This is logger.severe().");
10 }
11}
1$ javac Main.java
2$ java Main > stdout.log 2> stderr.log
3$ cat stdout.log
4$ cat stderr.log
5Nov 30, 2024 10:28:42 PM Main main
6INFO: This is logger.info().
7Nov 30, 2024 10:28:42 PM Main main
8WARNING: This is logger.warning().
9Nov 30, 2024 10:28:42 PM Main main
10SEVERE: This is logger.severe().
A more popular logging library in the Java ecosystem is Apache Log4j. Log4j allows routing events of different levels to different destinations via Appenders. The ConsoleAppender logs to stdout by default.
1package com.example;
2
3import org.apache.logging.log4j.LogManager;
4import org.apache.logging.log4j.Logger;
5
6public class App {
7 private static final Logger logger = LogManager.getLogger(App.class);
8
9 public static void main(String[] args) {
10 logger.info("This is an INFO log message.");
11 }
12}
1$ mvn package
2$ java -jar target/log4j-demo-1.0-SNAPSHOT-jar-with-dependencies.jar > stdout.log 2> stderr.log
3$ cat stdout.log
4WARN StatusConsoleListener No Loggers were configured, using default. Is the Loggers element missing?
522:36:47.309 [main] INFO com.example.App - This is an INFO log message.
6$ cat stderr.log
C++
In C++, the standard library clearly distinguishes stdout and stderr. std::cout writes to stdout, while std::clog and std::cerr write to stderr.
1#include <iostream>
2
3int main() {
4 std::clog << "This is std::clog." << std::endl;
5 std::cout << "This is std::cout." << std::endl;
6 std::cerr << "This is std::cerr." << std::endl;
7 return 0;
8}
1$ g++ main.cpp -o main
2$ ./main > stdout.log 2> stderr.log
3$ cat stdout.log
4This is std::cout.
5$ cat stderr.log
6This is std::clog.
7This is std::cerr.
spdlog is a popular logging library in C++, but it logs to stdout by default.
1#include "spdlog/spdlog.h"
2
3int main() {
4 spdlog::info("This is spdlog::info().");
5 spdlog::warn("This is spdlog::warn().");
6 spdlog::error("This is spdlog::error().");
7 return 0;
8}
1$ g++ --std=c++17 main.cpp -o main -I/usr/local/include -L/usr/local/lib -lspdlog
2$ ./main > stdout.log 2> stderr.log
3$ cat stdout.log
4[2024-11-30 21:40:28.441] [info] This is spdlog::info().
5[2024-11-30 21:40:28.443] [warning] This is spdlog::warn().
6[2024-11-30 21:40:28.443] [error] This is spdlog::error().
JavaScript
In JavaScript, Node.js sends debug and info level messages to stdout by default, while warn and error level messages go to stderr.
1console.log("This is console.log().");
2console.info("This is console.info().");
3console.warn("This is console.warn().");
4console.error("This is console.error().");
1$ node app.js > stdout.log 2> stderr.log
2$ cat stdout.log
3This is console.log().
4This is console.info().
5
6$ cat stderr.log
7This is console.warn().
8This is console.error().
Python and Ruby
In Python, the standard logging library logs to stderr by default.
1import logging
2
3logging.info("This is logging.info().")
4logging.error("This is logging.error().")
1$ python app.py > stdout.log 2> stderr.log
2$ cat stdout.log
3$ cat stderr.log
4ERROR:root:This is logging.error().
In Ruby, the standard Logger class requires explicitly
setting the log destination, such as Logger.new($stderr)
or Logger.new('app.log')
.
Closing Thoughts
This exploration reveals an interesting pattern across programming languages. The key takeaways are:
-
Don't rely on stderr to determine process success. Unless you know what you're doing, avoid using a program's stderr to determine whether it succeeded—rely on the exit code instead.
-
Most standard logging libraries default to stderr. Across languages like Go, Rust, Java, C++, and Python, standard logging libraries default to stderr. However, some popular third-party libraries, such as Log4j (Java) and spdlog (C++), default to stdout.
-
Be explicit for clarity. For consistency in your applications, explicitly configure logging destinations rather than relying on defaults.
The convention of logging to stderr follows the Unix philosophy: reserve stdout exclusively for program output, while using stderr for diagnostic information. This separation enables clean data processing pipelines where stdout can be piped to other programs without interference from log messages.
Thank you for reading! Your feedback is greatly appreciated, feel free to comment.