A debugger is a nice tool, but sometimes completely impractical. My scenario's for that have been:
- Debugging protocols or interfaces to them (timeout's etc.)
- Systems that can't miss any interrupts (in particular ones that are generated externally)
- Systems where a halt will make electrical smoke

Trace is nicer on real hardware. I once wrote a trace utility with focus of avoiding printf() in code, but still be able to trace serial messages with arguments in them.
For that I wrote a C macro table with string printf message and an argument list by types. Only data on target was a __COUNTER__ message ID and argument list .
Then in code a trace function is called, which takes the message ID and va_arg list, and print the corresponding argument list over serial in binary. The trace function can be very quick, because it's only moving raw bytes from va_args to a buffer. The data was circular buffered, and when CPU time was free the buffer is dumped over serial @ 750k - 1MBaud.
On the other end I had a simple FT232RL cable. PC ran a small script that also parsed the C-macro configuration file, and did the printf() on the PC.
Smallest trace was ~4 bytes of traffic (included some sync's), which isn't great in particular, but e.g. when printing 8x 16-bit integers it would only grow up to 20 bytes (instead of ~44 bytes in ASCII with no human formatting, plus lots of printf time)
For protocol debugging it's very nice if you can simulate the high-level protocol in a simulator, and write some unit tests for the code. For my home IoT network I made a complete "RF environment" down to the MAC level, and could instantiate multiple nodes at once. A nice bonus: when I halt the code ALL threads are halted and I can look at the state of each particular node.
Spreading assert() around your code at sane points is also quite nice. It only costs a little/no CPU time, can easily be turned off (just redirect assert macro to a void expression), and you get instant alerts about your code breaking at some points. It also makes you think about the range of arguments a particular function can support.