As someone who's been told my several people my debugging skills are impressive, I reflected on this a little bit and here what seems to work for me :

1) know your debugging tools. IMO this is imperative, as you're going to set probably thousands of breakpoints over your career, and use the continue/step functions millions of times. I personally use the eclipse integrated debugger. The "back-in-time" debuggers have proven too slow for the kind of product I work with.

2) Try to find a case that does the right thing. It's always useful to be able to compare 2 use cases, even though it might be a bit overloading at times. It might also help you understand what works and what doesn't, reducing the potential target area. And it'll give you a test case. Too many people write the test case after having "fixed" the bug, but end up using either a test case that was working to begin with.

3) make assumptions. Don't dig into every function on the code path. Step over a function, knowing what result you'd expect if the function was doing the right thing and what you'd expect if it was doing the wrong thing. If it's doing neither of these, either you don't understand the function/product as well as you thought or the previous developer didn't help you. If it's doing what you believe is the right thing, don't worry about that function. Move on.

4) Don't go for the long winding code path. If there's 2 ways a function could fail, you might want to go down the easiest to prove/disprove rather than the most likely to fail. If you can prove the bug is in the small path, you're basically done. If you can prove it's not, then you can start going down the rabbit hole. If you can't prove either way, maybe try to assume the bug is further down the execution path.

5) keep track of your previous assumptions. I like the eclipse debugger because I can see all my breakpoints at once, even when they span files. I can also deactivate some without losing their location. Sometimes, you'll have to go back to your previous assumptions, either because they were wrong or because your brain is trying to process too much information.

6) that brings me to something that saved me so many times : Don't hesitate to restart from scratch. It'll never be really from scratch. When going through the metal process again, you'll remember why you mad that assumption, you'll see paths that seemed curious but that you ignored. You might be able to cleanup a lot of the breakpoints you deactivated but didn't delete. None of that is wasted time

7) take a breath. Sometimes, I physically feel odd. A ball in my chest, my head goes heavy, I have some sort of anxiety attack. I've decided to think all those are just a sign my brain is getting overwhelmed. Take a breath, get some water, take a short walk, and go back to 6. You might have made a wrong assumption, or you're just trying to keep too much data in your brain. Just restart and get back to where you were again. Usually you'll be much faster the 2nd time than the 1st time, and faster the 3rd time, etc.

8) if you find a solution that's like "if value is one specific value, then return", you're probably on the right path but you're not fixing the whole thing. Which means you or someone else will be doing exactly what you did. If it's you, you'll just get frustrated. If it's someone else, they'll probably do a worse job than you, or hate you for your hack, or just add one more hack to yours. Dont start the nasty if statement. Sometimes it's what you need to do, but that's pretty rare in my experience.

9) if a sudden idea sounds completely absurd, unrelated to the current code path at end, or stupid but is overly easy to test, drop everything and do test that idea. Because you still have all your breakpoints visible, restarting from scratch won't be that long, but the stupid idea might save you a lot of time. An example from no later than yesterday : After debugging something that seemed impossible, we realized we were simply not connecting to the right database. Stupid to no point, but super easy to test and gave us the right answer right there. Feel free to laugh at yourself, but don't blame/shame yourself or the person you're helping. Just tease them/you a bit and move on.

10) don't hesitate to debug using printf. I know it's frowned upon in certain circles, but if a specific line is being hit hundreds of time by your smallest test case, hitting "step" or "continue" every time will prove too long. I personally print something along the lines of "MOLYSS 10a : this is what I'm doing and here's the value of the variable I'm interested in : ". No one else in the entire company should be printing anything starting with MOLYSS, so it's super easy to grep my logs for it. If you think a code path is very unlikely, just put "WTF" in your printf output, so you know you need to understand why you're printing it in the first place.

11) know what a race condition is. Learning to recognize one when you see one has saved me many times. These are often extremely hard to prove, but fixing them might just be about adding a dependency or a synchronized block.

12) feel free to be proud when you found something. No one will pat your back, so you got to do it yourself. It's one of these skills that few people see but that are crucial and that can make you the person to go to for tricky/urgent issues. And when Sev0 always end up on your plate not because you're responsible for the bug but because you're the most reliable around, it gets noticed.

I'm probably missing a few things, but I think if you can do all that, you'll probably be better and faster at debugging than many people around you