The better test alternative to slog.DiscardHandler

September 17, 2026

Last time I described slog.DiscardHandler as a viable way to silence logs in tests.

Now I want to tell you why I never use that option, and you probably shouldn’t, either.

But first a story.

The slog package was introduced with Go 1.21 in August of 2023. It didn’t yet have slog.DiscardHandler, so you had to roll your own. But that was easy:

logger := slog.New(slog.NewTextHandler(io.Discard, nil))

Easy as it was, it was also annoying. So in Go 1.24, slog.DiscardHandler was added, making it slightly easier.

logger := slog.New(slog.DiscardHandler)

But then in Go 1.25, something even better was introduced, and it’s what I always use now. But it came from a different place: testing.T.Output

What does T.Output do, and how does it relate to logging?

func (*T) Output

func (c *T) Output() io.Writer

Output returns a Writer that writes to the same test output stream as TB.Log. The output is indented like TB.Log lines, but Output does not add source locations or newlines. The output is internally line buffered, and a call to TB.Log or the end of the test will implicitly flush the buffer, followed by a newline. After a test function and all its parents return, neither Output nor the Write method may be called.

(Note the same method exists on testing.B, and testing.F as well).

In most cases, logging to t.Output() is always better than logging to the discard handler. In the usual case, the observable output is identical: Nothing.

But, in the case your test fails, the log output is now included in your test output. And that can be very valuable for debugging or diagnosing a test failure.

logger := slog.New(slog.NewTextHandler(t.Output(), nil))

What’s more, this trick isn’t limited to slog. You can use the exact same t.Output() writer for the log package, any third-party logging package, or any other writer that has nothing to do with logs at all!


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


slog.DiscardHandler

Now that we’ve made it through the Handler interface, let’s look at the first of the implementations provided by the standard library: the DiscardHandler. var DiscardHandler Handler = discardHandler{} DiscardHandler discards all log output. DiscardHandler.Enabled returns false for all Levels. Why would you ever want a discard handler? Two likely scenarios come to mind. The one where I most frequently use it, is testing. You may not wish to log anything during a test, and this is a simple way to do that:


Expanding errors with ReplaceAttr

Let’s look at one other example where I reach for ReplaceAttr regularly: Logging additional error detail. (I talk more about this approach in my Boot.Dev course Learn Logging and Observability in Go (see below for a discount code).) For this technique to be meaningful, I first need errors that contain additional details. And this can take many forms, but probably the most ubiquitous form is an error that contains a stack trace, such as those produced by the popular package github.


Test logging with ReplaceAttr

Today’s topic is probably the most interesting, subtle, and complicated of the three HandlerOptions fields: ReplaceAttr. type HandlerOptions type HandlerOptions struct { … // ReplaceAttr is called to rewrite each non-group attribute before it is logged. // The attribute's value has been resolved (see [Value.Resolve]). // If ReplaceAttr returns a zero Attr, the attribute is discarded. // // The built-in attributes with keys "time", "level", "source", and "msg" // are passed to this function, except that time is omitted // if zero, and source is omitted if AddSource is false.

Get daily content like this in your inbox!

Subscribe