printf is not the only debugging tool. It's useful when you're testing a hypothesis, and when you can repeatedly and rapidly reproduce the failure. An automatic test runner, such as Karma helps here. In JavaScript, console.trace complements console.log. Other languages have equivalents.

Debuggers are great for exploration. They let you look up and down the stack. They make it cheap to peek at all manner of variables without cluttering the output. They allow you to Go to the source. They don't swallow type information the way stringification for printf does. It happened twice in one week that I spent fifteen minutes trying to find a bug in my Ruby code by running through it in my head. Each time I finally fired up Pry with pry-byebug and found the problem within a minute in a place I hadn't expected.

The developer tools in the browser are known to everyone, I hope. I use the console, the network tab and the debugger all the time. Sometimes using the debugger appears to change the behaviour of the system. I haven't gotten to the bottom of this. In these cases, I have to fall back to printf and friends.

Verbosity is similar to printf, but sometimes you can't get the information you need by adding printf to your own code. For example, there were two cases when I needed to know which test case was causing a hang or a certain warning. printf or a breakpoint didn't help, because I didn't know where to add them. The abovementioned Karma normally doesn't tell you which test it is running at any moment, but in debug mode it does. Minitest only gives you dots, but with minitest-reporters it, too, logs the test names as it runs.

Summary

There is a hurdle in front of everything but printf. Setting up the tool, learning the tool takes effort. But it's worth it. Once upon a time I tracked down an intermittent failure that had been bugging the team for two years. I did it by connecting Winpdb over SSH to a Python process running in a Docker container during a Jenkins build. As if this is not enough, the failure was occuring in Python code within a YAML file, where the debugger couldn't jump directly. So I had to devise a way to gingerly step through the interpreter of the YAML data without messing up the setup and having to restart. – Jump those hurdles.