I taught the practice of web development to around 200 students in six years. Here are ways to improve on the mistakes and misconceptions I noticed most often.
TL;DR
- Use automatic code formatting
- Look for error messages
- Simplify your code until the bug goes away
- Solve only one problem at a time
- Split tasks and commits
- Give symbols a proper name
- Refrain from DRYing everything out
- Optimize reasonably
- Test the risky parts first
- Avoid persisting redundant data
Use automatic code formatting
Even if you work alone, automatic formatting is a free time saver: you can write code faster, and will be able to read it more easily later.
If you write JavaScript or TypeScript, install the Prettier extension for Visual Studio Code and enable "Format on save".
That's all, you don't need a configuration file, but you can add one at the root of your project if you need to override the default rules.
Look for error messages
When the app doesn't work, look for an error message. This is especially true when your app is made of multiple services.
Let's say your Next.js web app with server-side rendering relies on your back-end server. When something fails, here are four places you have to look for an error:
- the browser console (error on the client)
- the server response body in the inspector's Network tab (invalid request error, it should appear in the UI)
- the Next.js server console (error during SSR)
- the back-end server console (error on the back end)
Simplify your code until the bug goes away
What to do if you can't find any error message, or if the cause of a bug remains obscure after searching the error online?
Simplify your code (for instance, comment out some part that runs when the bug happens), and try to reproduce the bug again. If reproducible, repeat again and again until the bug is gone.
Sometimes, you only need to do this once to find the faulty part in your program.
Some other times, you simplify your program so much that it looks like a Hello world boilerplate and yet the bug is still here.
Try updating dependencies or changing environment variables. If you are still unlucky, post your issue on Stack Overflow or, if applicable, on your framework's repository issues on GitHub, and provide your simplified code.
Solve only one problem at a time
If you try to simultaneously fix a bug, implement a new feature and refactor code, chances are you will waste time by breaking your code or mix unrelated changes in a single commit, which will make code review harder.
If you feel the need for a refactor or a bug fix in the middle of an unrelated feature, refrain from doing it. Save it for later, preferably by opening a dedicated ticket (you can automate it with a GitHub action that will create an issue from any "TODO" mention in your code, for example TODO to Issue).
Split tasks and commits
When you receive a ticket for a new feature, work on it before you start implementing it. Add a checklist of subtasks and cases to handle, and implement them one after the other. You can also use this detailed case-by-case specification to write automated tests. This will help you keep going when the task at hand is overwhelming.
The same discipline goes for commits for large features: help the code reviewer by splitting your work into commits with a clear title.
Give symbols a proper name
Symbols are all the phrases that you define in your code: types, interfaces, constants, variables, functions, classes, methods.
The name data
is rarely helpful when reading code. Take some time to give a descriptive name to your data and don't forget to use the plural for arrays.
Function and method names should start with a verb that describes what is returned (and/or performed).
Do not be afraid of longer names if necessary: Everything should be made as simple as possible, but not simpler. (Einstein).
Follow these rules and your code should read almost like a sentence in English.
Refrain from DRYing everything out
It is tempting to deduplicate everything in your codebase because someone said "Don't repeat yourself".
You can have rules that look the same but are conceptually different. If you refactor them into an abstraction, they could become harder to understand and maintain.
Rather than DRY, make your code ETC: Easier To Change.
This means using a single source of truth for things that are conceptually the same, for example: duplicate constants, business rules or UI components. If you copy-paste them, they will become hard to change.
Optimize reasonably
Just like excessive DRYness, premature optimization is a waste of time.
You might want to improve the performance of an algorithm, for instance by replacing a loop with a regular expression.
Is the performance gain valuable to the user in real-world conditions, with actual data? Is the gain offset by a potential loss in code readability?
A lot of time, micro-optimizations are not worth it.
A rule of thumb to avoid poor performance before it hits you in production:
- keep the time complexity of the algorithm under O(n log n)
- if you run database queries, do not run them in a loop of the size of the input but use a fixed number of join queries instead
Test the risky parts first
What part of my code should I test?
Again, make good use of your time. Test what is most critical and/or most at risk of failure.
You can start by testing the core business rules of the application, especially if they are supposed to evolve over time (they are at risk of regression).
Also, when a bug is reported, you can start by writing a test that highlights the bug (which means it should fail) before fixing the implementation (test-driven development).
Should I test all cases?
Test cases that are expected to happen in real-world usage, starting with the most important ones, business-wise.
Avoid persisting redundant data
Again, aim for a single source of truth.
Whether you're developing a stateful user interface or a service connected to a database, you're going to persist a state (in memory or on the disk).
In React components, I have seen many times a filtered array set to state whereas it could be calculated at render time. Instead of setting in state only the filter arguments, both the arguments and the derived array are stored, making code more verbose and error-prone because both state values must be updated everytime the filter arguments change.
Similarly, I have seen redundant database design, where data is set in a column whereas it could be derived (calculated) in real time.
However, while this single-source-of-truth approach is conceptually cleaner and easier to maintain, it can lead to performance issues if the calculation is heavy and repeated multiple times with the same arguments: why recalculate it if the result remains the same?
In this case, you can cache the result of the calculation using memoization: for instance useMemo
in React, which will automatically refresh calculation when arguments change.
In a database, for instance Postgres, you can use materialized views, but you will have to refresh their content manually.
Summary
- Actively look for error messages ; don't be afraid of them
- Keep your work going by splitting tasks into smaller units that are easier to complete
- Think of the person that will read your code: favor readability over excessive factorization or optimization
- Beware of premature optimization: use your time to fix perceivable problems
Top comments (0)