Logs default to stderr in Go and other languages: avoid using stderr to determine program success.
It's a beautiful day, and it began with a short code review.
1# tools/foo/main.go
2- fmt.Println("found it")
3+ log.Println("found it)
The author explained advantages of using a logging library over the plain old 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. That is intriguing, let's take a closer look.
The foo
tool is used by another tool, bar
. Here’s a simplified version of bar:
1// tools/bar/main.go
2func main() {
3 fmt.Println("going to execute some command")
4 cmdName := os.Args[1]
5 cmd := exec.CommandContext(context.Background(), cmdName)
6 var stdout, stderr bytes.Buffer
7 cmd.Stdout = &stdout
8 cmd.Stderr = &stderr
9 if err := cmd.Run(); err != nil {
10 fmt.Printf("exec error, %v, %v\n", cmdName, err)
11 os.Exit(1)
12 }
13
14 if s := stderr.String(); s != "" {
15 fmt.Printf("exec error, %v, %v\n", cmdName, s)
16 os.Exit(1)
17 }
18}
The bar
tool 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 output of foo. However, the default behavior of
Go’s standard log library is to write logs to the standard error stream (stderr).
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.
For simplicity, this blog focuses on the default behaviors of logging libraries. Note that all the languages and libraries mentioned in this post 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 logs with ERROR or higher to go to stderr. Curious about this behavior, I decided to look at a few popular Go logging libraries: zap, logrus, zerolog. It turns out that all three libraries also log to stderr by default, aligning with the standard log 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 began to realize that logging to stderr is both intentional and conventional. The philosophy seems to be to reserve 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 such as info!
and warn!
, but it
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 versatile and popular logging library in the Java ecosystem is Apache Log4j. Log4j allows logging 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] ERROR com.example.App - This is an ERROR log message.
6$ cat stderr.log
C++
In C++, the standard library clearly distinguishes stdout and stderr. std::cout logs to stdout. std::clog and std::cerr logs 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, by default, Node.js logs debug and info level messages to stdout, warn and error level messages to stderr.
1console.log("This is a console.log().");
2console.info("This is an console.info().");
3console.warn("This is a console.warn().");
4console.error("This is an console.error().");
1$ node app.js > stdout.log 2> stderr.log
2$ cat stdout.log
3This is console.log().
4This is console.debug()
5This is console.info().
6
7$ cat stderr.log
8This is console.warn().
9This 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.debug().")
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. For example, Logger.new($stderr)
or Logger.new('app.log')
.
Closing thoughts
It is fun exploring whether the default logging destination is stdout or stderr.
- Unless you know what you are doing, avoid using a program’s stderr to determine whether it exited successfully, rely on the exit code instead.
- Across languages like Go, Rust, Java, C++, and Python, standard logging libraries default to stderr. However, some popular libraries outside the language SDK, such as Log4j (Java) and spdlog (C++), log to stdout by default. For clarity and consistency, it’s best to explicitly set logging destinations.
Thank you for reading! Your feedback is greatly appreciated, feel free to comment.