How to write elegant code and not drive yourself or your peers crazy

Whether you are just starting out or a veteran at writing code, keep your fellow software engineer’s (and your own) sanity in check with these pointers.

Sufiyan Rahmat
Reflex Media
7 min readJun 4, 2021

--

Code readability, maintainability, and reusability is important; be it writing solo or with a team size of 20 developers. Always assume that the next developer looking over your code does not like the way you write them, and is now up to you, to prove otherwise through writing elegant code.

Photo by Christina @ wocintechchat.com on Unsplash

While these pointers can be used in any programming language or framework, the examples will be written in Node.js with ES6 syntax.

1. Descriptive function names

Let your function describe itself. This will also eliminate the need to have unnecessary comments on every single line of code or code blocks.

Based on the above getUserById(), one can safely assume that the function returns the User model by its Id.

Now compare that with this example:

That comment is now a necessity; as without it, no one will not be able to understand just what exactly is going on without looking deeper into the underlying logic of gtU(). Even yourself will likely have forgotten what it does 2 years down the road.

2. Function should reflect its own underlying logic

Continuing from the previous example, any developer reading the code should safely assume that the function accepts an identifier; that is likely a primary key for a specific User, and that it returns the User model object. All of these understanding without having to look into the underlying logic itself.

This is how the underlying logic should look like:

Now this is NOT how it should look like:

Why? The returned object contains only a small subset of the User model, which is misleading to the caller. The proper way is to ensure to return the entire db record is returned instead.

But what if you only need a select few like the incorrect example? Well, the answer is to be descriptive. Create a new method with this function name instead: getUsernameEmailById().

Problem solved.

Let’s look at another example of a poorly or ambiguously descriptive function name.

The function describes itself as “Return true|false if given username exists on db”. However, the returned response is a User object. Instead, this is what the function should have done, based on the function name.

Better yet, use TypeScript!

With TypeScript, fellow developers (and yourself in the future) should be able to see upfront the expected function arguments and its response.

Now your favorite IDE should be able to inform you the expected function arguments and response at time of coding.

3. Keep it slim

Keep your code slim by isolating or moving complex logic out of the way. Not only will you make your code reusable, it will also be easily maintainable and readable.

The above example is not only readable, it is also maintainable, with very few lines of code. One can tell the sequence of createNewAccount() immediately. Any changes to any of the underlying function can be done without much impact on the other functions, and is thus reusable. Writing unit tests will also be a breeze!

Now the same function, written without the readability, maintainability, and reusability in mind, will look something like this.

Shudders.

It will take anyone quite sometime to understand just what is going on. There are also lots of unnecessary details that might not be relevant to the logic itself for creating a new user account.

4. Return early

Keep your logic simple. Avoid unnecessary nested if/else statement by returning early.

This is a common example of a traditionally written logical flow.

Notice how confusing it is to read and understand what exactly is going on with that code. Now let’s refactor that by returning early.

Notice how flat the code looks like now? By throwing errors early on, we can avoid having to read the entire logic to get the idea. Reading from the top-down will also be much more easy to understand what the createNewAccount function is trying to achieve.

The code can be further optimised with even lesser LOC.

Look at how clean and beautiful that looks! Combining with the earlier pointers, it is easy to tell the logic for createNewAccount() through descriptive function names.

5. Return immediate without variable assignment

If there is no intention to use the returned data within the function, simply return it without first assigning it to a variable.

Now compare this with an unnecessary variable assignment:

Notice that the latter example will need to make use of an async/await as the db.insert() returns a Promise, while the former example does not, as the Promise will simply now be handed off to the caller.

However, if there are other logic that needs to be further processed, then this approach no longer applies.

6. Isolate 3rd party dependencies

As much as possible, place any 3rd party libraries or dependencies into a services or utils directory. This will allow you to name a generic function without being dependent on any one specific 3rd party or logic.

Let’s start with a simple example of doing an email validation check. Assume you have installed an npm package named ‘email-check-lib’ and decide to integrate within your app.

Without service isolation, this is how it will look like in your createUser() function.

That same email validation logic is also required in your updateUserEmail() function.

Now imagine there is a newer, better library out there or that your colleague decides to just simply use regex instead of using a 3rd party library. You or your colleague will then need to update on every place that uses that library.

Painful… but totally avoidable!

Avoid unnecessary pain by writing it into a common utils directory.

Now your createUser()and updateUserEmail() functions can simply be written as such:

Notice that you do not even need to check if it is valid or otherwise, as your newly created validateEmail() utils function is expected to throw an error if it is not valid. Also with this approach, you can change the underlying logic to validate email anytime desired without impacting any callers.

Perfect!

7. Use async/await where possible

Async/await is readable. That’s it.

Notice how the 2 Promised functions are called in a flat and logical manner? Now compare this using traditional Promise callbacks.

Notice how the 2 promised functions will now have to be nested within the callback function.

Another benefit of using async/await is that the function response is automatically wrapped as a Promise.

Even though the function above returns either a string or throws an Error, because it is an async function, the response will be returned as a Promise.

Without using async/await, this is how you will need to return the same string response as a Promise.

Notice how you will need to manually wrap the response as a new Promise function.

8. Strategic try/catch

There are 2 kinds of newbie developers. The kind that will place try/catch for every line of code, and the kind that does not have try/catch at all. An experienced developer will only do a try/catch strategically where it makes sense. Also thanks to Rowland Smith for pointing them out!

Not having a try/catch with proper error handling is bad practice as we do not have control over the kind of response that will be sent back. While nothing wrong with having a try/catch block for the entirety of the function, it would make much more sense to place them where it would make sense.

Note that there are many opinions and strategies for how try/catch should be placed and nothing wrong with them. But here, I will explain the approach that our development teams have adopted.

Nothing wrong with the above example. However, you will soon loose finer control in how you would want each error to be handled.

The first thing I would do is to move the const user out of the try/catch as I do not expect any errors at all coming from that. TSLint, ESLint should have caught that for you instead.

Next, I would want to inform the user if the format is valid and if the username is available.

Next, we’ll need to update the user should the insert itself has failed.

Lastly, I decided that a sendMessage() is good to have, it is not necessary for my application, thus it should be allowed to fail.

Putting it all together, this is how it would look like.

It might seem like there are too many try/catch block. But think about it. If you want to inform the end user which step of the way is having issues, which would that be?

Also notice how some lines does not require a try/catch (declaring const user), some using a custom Error class, and how some can continue without breaking even when an Error is thrown (last try/catch). This now gives you finer-grained control of how you would want to handle specific problem areas.

Final Thoughts

No two persons are the same. The same goes for best practices when writing code. Best practices are at most subjective to the person writing it. What is more important is that the individual, team, or even organization set a common theme around what’s best and what works for themselves and stick to it. You may, or may not agree with any of the above pointers, but do agree or disagree as a collective within your own team or organization.

If you have other pointers not mentioned here but felt unease when reviewing them, do comment and I will add them in too! Also if there are some of these pointers that you do not agree, let’s discuss on it and learn together!

Cheers and have fun coding!

--

--

Sufiyan Rahmat
Reflex Media

VP of Software Engineering. Developing high traffic, consumer apps with developers across the globe from Singapore, Philippines, India, and United States.