A Refactored Approach

Here’s one way I might refactor the above:

Example 4. Promises, Refactored.

What? Why?? WHY would I take such a small bit of code and blow it up like this???

At first glance, this looks even more complicated than the other examples. It’s certainly more code, but I assure you I have some good reasons.

How I Think About Refactoring

Below are some of guidelines I consider when writing promises. With these in mind, hopefully my reasoning for “Example 4” above will become clear.

Name All The Things

Avoid anonymous functions like the plague. Sure. Go ahead and use them while you’re spiking on an idea, playing around, or otherwise writing throw away code, but when it comes time to commit, ensure everything has a meaningful name.

You may want to do the same for variables. Name them, and break them out instead of writing them in-line. With variables it is not always necessary.

The thing to keep in mind is whether or not you are writing code which can be easily debugged. It’s not about style, or compulsion. Ask yourself if the operations you’re performing are easily traceable and if the data you’re operating on is easily observable in your debugger. If the answer is yes, you’re done.

If you review “Example 4" above, you’ll see that I gave all my functions a name to ensure traceability.

Keep Things Flat, Keep Things Small

Avoid the “pyramid of doom” which too often accompanies async code. This means not only should you stop using anonymous functions, you should move them outside of the calling function body whenever possible. If you see your code marching further and further to the right of the screen, stop what you’re doing and start flattening things out.

If you name your functions as described above, and avoid reaching out of scope, this is a trivial exercise. Once un-nested, you’ll likely find you’re able to easily separate control-flow from other logic. Having separate, clear, readable control-flow makes your code more understandable. Notice in “Example 4” how the control flow is very obvious and completely separated from the details of the implementation.

You also gain the ability to reuse parts of your implementation. The extracted functions become modular. As your project grows, you’ll be in a better position to make further use of the code you’ve already written.

Lastly, un-nesting your functions yields a more performant implementation. As I mentioned above, there will be less objects in memory for the garbage collector to concern itself with.

Of course, as with every rule, there are exceptions. The exceptions to this rule are helpful functional patterns, such as currying, which require nested functions. Both curried functions and thenables are unary and can be composed, nicely working around the limitations of promises.

If you again review “Example 4,” you’ll see I’ve un-nested all of the function calls from the previous examples and re-worked them into more reusable, modular pieces. You’ll also note that I took advantage of currying to carry the car variable through to a downstream handler.

Following this guideline makes for more code right now, but I have the opportunity to reuse some of this in other handlers down the road, and, in the meantime, I gain better debugging capabilities, better performance and a clearly stated control-flow.

Contain Complexity

When you follow the above guidelines, you automatically contain some of your code’s complexity. That said, you should always be on the lookout for other ways in which you can hide away the more complex logic — in particular, try and keep it out of your control-flow.

Consider again “Example 4.” If this were production code, I’d want to lean on some other libraries, such as bluebird, lodash and/or ramda, to move the complexity of the curried functions out of sight.

Log Complex Functionality

Sometimes there’s no getting around the complexity that can accompany async code, no matter what patterns you use. Debugging async code can be hard, so don’t be afraid to add some logs inside complex functionality.

Your goal should always be to make your code as easy as possible to debug. That said, be aware that while logging can work for you, it can just as easily work against you — be judicious about what you log. Too many logs and you loose your ability to gain meaningful insight. It is too hard to separate the signal from the noise. The right logs, however, can drastically increase your code’s debuggability.