Building my previous Combat Automation post, one of the biggest programming mistakes I come across is the over use of nested "if/else/try/catch/.." statements.
The over use of indentation makes an application "feel" more "engineered". If you started out in languages like C, or you were writing code in the pre-2000 era, these optimized code paths made sense. Chances were however, you were one of the few people that needed to read your code and that's probably the only job you had as a programmer. As compute prices near the zero marker, the cost balance between efficient code and code that's easy to debug starts to flip.
In order to debug more complex code you need to keep track of that nested code path in your head while you traverse. If you're 25 years old, this is easy. If you're 35 with 3 kids, it becomes much much harder and writing code isn't your day to day job. Especially if you wrote that code when you were 25 and have to fix it when you're 35. If you're a fully staffed "full stack applications team"; this is your full time job. If you're in opsec.. chances are it's not.
When you're starting a project [and when you're younger] complexity is cheap and refactoring is expensive. You don't refactor a problem you're not quite sure of the answer to. However, as the solution grows in complexity, that balance shifts. You spend more time trying to think through what you wrote and less time solving the problem. As you start bringing more partners into the solution, they also have to digest that complexity before they can help with the solution too.
You think to yourself, "self: we should refactor this!", then the fear sinks in. "Where do I start? How do I refactor without breaking the code path?". While there's a semi-obvious solution to this in writing tests, tests alone doesn't solve the immediate refactoring problem. Your tests will prove you made a change that broke your code, but the test won't suggest WHERE your code path went sideways. To solve for the where- you have to get all those code paths into your mind first, so you can begin to sort through the logic.
One of the best patterns I've come across in the past few years is the concept of "return early". If you find yourself in a function with too many if/else statements, see if you can break some of those out tests early in the function. Testing for a null? return out first thing. Testing for a true or false? Return out right at the beginning. This reduces the number of indents, brackets or semicolons our brain has to keep 'in memory'.
An additional benefit to this pattern, as our function grows in size and complexity, it naturally lends itself towards refactoring. As you see the number of return statements grow, your first inclination is to wrap them into their own "pre-check" function. This makes the real work your function was built to perform that much easier to digest. The function only processes the logic it was meant to, leaving the pre-check work completely to another function.
It also cleans up your testing process, you can test the pre-function separately from the real function itself. As the number of these functions grows, they can be abstracted into lower level libraries [and tested] outside of your main application. The less code you have to read, the less mistakes you will make. The less mistakes made and the faster you can absorb changes, the faster your code will be digested by others and the faster they'll be in a position to give you good feedback. Good code begets more good code.
Combine this with a good CI process and you'll never fear accepting a small and precise pull request. The changes will be obvious and assuming the tests pass, everyone will get that warm fuzzy feeling of progress. Less stress, less frustration, more confidence.