Code quality through 7 guarantees
Code quality from the standpoint of guarantees you can provide about your code

When you are done writing your code, ideally you’d like to push it straight into production and call it a day. Unfortunately, this is rarely the best course of action. For most of us, finishing the coding process is just a start of the final verification steps necessary to ensure our code is free of defects, usually in the form of manually kicking the tires to make sure everything works, and that nothing else is broken.
Confidence that the code is defect-free is not a yes or no thing. It lies somewhere on the continuum between no confidence at all to the elusive deploy it and go home without giving it a second thought. While your skill as a developer certainly plays a big part in moving towards the positive end of the spectrum, there are a number of tools and practices that help you get there.
This article will cover some of the common techniques for bumping our confidence in our code. Each of these techniques provides some kind of guarantee about your code, and the best part is they are all more or less machine-enforceable.
What are guarantees?
A guarantee in this context is 100% assurance about the absence of defects. Anything less than 100% is not really a guarantee and cannot be relied upon in all situations. There are certainly many techniques that improve code quality, but here we discuss only those that come with some form of guarantee that is enforceable by the machine. These guarantees are, with some exceptions, usually secured in a mechanical way, by forcing our code down some path and rejecting paths that will lead to defects.
If I had to pick a synonym for the word guarantee as we use it in this article, it would be safety. Safety in the sense that we can perform some operation knowing that there won’t be defects given that some condition is met, and compatible with the usage in type safety.
1. Linters
Whether you use a compiled language or an interpreted one, you know there’s a defect in your code if it won’t run. If the compiler is unable to make sense of your code, it’s safe to say you’ve got a bug in there.
Linters are a quick way to find out about such problems before your code even reaches the compiler. Most linters will need to parse your code similar to how a compiler would, so if it’s unable to parse the code, it’s a guarantee that your code won’t be able to compile as well.
If your code is able to pass the linter, you have a guarantee that it will compile.
It’s not much of a guarantee, but combined with editor plugins, this makes for timely discovery of common issues such as typos, which is better than nothing.
Some linters can also prevent defects by flagging incorrect usage of some of the language’s constructs that are known to cause bugs in most situations, but this is normally the case with languages that provide such constructs to begin with.
2. Assertions
Assertions are functions that let us claim something about the values in our program and cause the program to explode in our face if the claim turns out to be untrue.
Let’s say we have a piece of code where we want to work with two values, x
and y
, and where x
needs to be larger than y
. We could insert an assertion somewhere like this:
console.assert(x > y)
If we reach the above line and x
is less than or equal to y
, an exception is thrown.
It may sound somewhat useless, but keep in mind that we make these assertions as an expression of our basic assumptions about our program that must always hold true. In other words, this is a way of discovering conditions where our basic assumptions are failing during development.
Many programming languages provide ways to make assertions natively, but they can be implemented manually as well. For example, a custom assertion function may look like this:
function assert(claim, message = 'assertion failed') {
if (!claim) {
throw Error(`Assertion error: ${message}`)
}
}
One reason to always prefer native implementations is that the traceback for native calls will always point to a more useful location in our code than the tracebacks for custom implementations.
Assertions provide a guarantee that our program will not fail with an exception if the values satisfy our basic assumptions.
Approaches such as Design by contract, formalize the use of assertions by prescribing what kind of assertions we need to make and where.
Important thing to note about assertions (as opposed to other techniques discussed later) is that it can cover a broad range of values and conditions. We’ll come back to this again when we talk about tests.
Assertions will not trigger unless you are able to reach the line on which they are made. This is why, while quite powerful, assertions are only able to do their job in carefully planned testing.
3. Total functions
Total functions are those that will produce a valid output across the entire range of its inputs. Functions that are not total are called partial, and they may sometimes return no value (or return a useless value like NULL
).
Partial functions can be a rather big liability in our code. In the best case scenario they increase the amount of code by introducing null checks, and in the worst case scenario an uncaught null value causes breakage in an unrelated part of our program.
Total functions guarantee that for any input we use, we get a usable output.
When talking about total functions, it’s important to differentiate between methods that provide us with a real guarantees as opposed to simply paying careful attention to how we write our functions.
In most programming languages we are able to write total functions by being mindful about our inputs. It is not always possible, however. TypeScript, for example, makes writing total functions harder, by making guards somewhat difficult to implement, and making implementation of Maybe monads very tedious if not impossible.
Some languages (especially statically typed functional programming languages) provide additional tools to deal with potentially partial functions. Mechanisms such as guards usually ensure that we have covered the entire range of inputs, and we are also able to use Maybe/Option monads and similar constructs to encapsulate null checks in a way that makes the function total.
4. Tests
Tests are probably the most talked-about approach for securing guarantees about your code. Tests are also the most overestimated tool in terms of the guarantees people believe it to provide. It should also be pointed out that tests are the most complex of the methods described in this article, and complexity itself creates an opportunity to introduce to defects.
Tests are essentially small programs that probe your production code in order to determine that there are no defects in it. They are only able to determine the absence of defects in the use cases for which you are able to write them.
It is important to differentiate between code coverage as opposed to case coverage. Code coverage is the extent to which different execution paths in your code were exercised during a test. Case coverage is the extent to which different use cases were covered during a test.
Tests guarantee the absence of defects in the use cases covered by the tests as long as there are no defects in the tests themselves.
As we’ve already discussed, tests can be complex. After all, they are programs, just like the program being tested, so the same rules apply. In order to keep the tests effective, we need to ensure their complexity is minimized. This aspect of tests is not in the domain of guarantees anymore, though.
We mentioned in the part about assertions that assertions are able to cover a broad range of values. Unit tests on the other hand are prohibitively expensive for covering such ranges. Imagine writing a test that covers all possible integers to test a function that takes a single integer value! This is why assertions and unit tests are not mutually exclusive, and in fact complement each other quite well.
In some cases assertions can make tests simpler because, with the right assertions, we can treat exceptions as failure conditions, without explicit failure cases. Tests can also be used to exercise the code containing assertions, which is a bit more challenging to do in manual testing.
4.5 Test-driven development
Test-driven development, or TDD for short, is yet another popular method of ensuring code quality. TDD is not quite mechanical as other methods discussed in this article, as it’s more of a practice than a tool. However, it still provides a level of guarantee that is interesting to note. Here we treat it as an extension of (or proper way of) testing.
TDD is an approach where we maximize the case coverage of our tests by writing test cases first, and then the production code. In TDD, we first write a single failing case. We then write just enough production code to satisfy this one case, and not a line more.
TDD guarantees 100% case coverage in our tests as long as we are disciplined about doing TDD correctly.
Needless to say, this requires quite a bit of discipline, and, since it’s not a machine-enforceable way of providing guarantees, it is not a safe method. All the caveats that apply to tests also still apply.
5. Static typing
Although static typing is a language feature, it is also a tool for providing guarantees about our code. Unlike some of the techniques which either work or don’t, the guarantees provided by the language’s type system is a function of the quality of the type system. We could also think of a type system as a set of different guarantees rather than a single guarantee, and some type systems providing more guarantees than others.
For instance, Elm’s type system comes with a guarantee that there will be no runtime exceptions if the code can be compiled. With Haskell, the code is said to “usually work” without exceptions if it compiles. With languages like TypeScript, you can have plenty of exceptions even if the code compiles.
What we get out of a type system is the ability to detect issues with composition/integration. We are able to tell if two functions/methods can work together. As long as we are working with constraints that the type system is able to infer (with or without our assistance), the compiler will be able to compile the code. If there are functions that cannot fit into the context in which they are used, compiler we refuse to compile the code.
Static typing guarantees that the code will not compile if there are issues with function/method usage detectable by the type system.
The key factor here is the “detectable by the type system” part. Type systems that are able to infer more about the types, are going to provide better guarantees about our code than those that require our full cooperation, simply because there is less human factor involved.
In relation to assertions, static typing replaces an entire class of assertions related to the type of values. Similarly, static typing replaces tests related to types (e.g., testing the behavior of a function that is fed a value of the wrong type is not useful in most statically typed languages). However, static typing is not a replacement for neither assertions nor tests.
6. Immutability
Code that mutates values can be quite a bug-mine. This is especially true of code that depends on timing that is hard to predict (e.g., multiple UI events that may come in any order). Another set of issues arise from sharing mutable objects across threads, which can leave our program in an inconsistent state.
In most programming languages, we have at least an option to use immutable data structures, and many functional programming languages enforce them. This allows us to rely on the stability of values throughout the course of a program’s execution, and prevents many of the issues with mutable objects.
Immutability guarantees that the value of objects will remain stable and cannot be randomly mutated by other code.
In languages like JavaScript, it is possible to treat objects as immutable without enforcing immutability (e.g., by always copying the objects). While the benefits are similar to that of using proper immutable data structures, we cannot talk about guarantees as long as our data structures are not physically immutable.
With immutable objects, assertions make even more sense, as they will hold true not only where they are made, but throughout the rest of our program.
7. Pure functions
Pure functions are functions which will only work with values passed to it, without side-effects, and for a given input, always evaluates to the same output. These functions are similar to functions in mathematics.
Pure functions have many useful properties that improve our code. Pure functions can be memoized, meaning that once we have the output, we can pair it to the input and avoid calling the function for previously used inputs. We are also able to test the function more reliably because we know that we’ll always get the same output for any given input, and we can be reasonably sure that we’ve covered the entire range of inputs by using only several critical samples of the input that are representative of the entire range. Similar to immutable data structures, we have assurance that a pure function is not going to mutate anything.
Pure functions guarantee that they will always provide the same output for a given input without observable side-effects.
Pure functions are a great companion to testing, and their guarantees stack nicely. Testing pure functions results in simpler tests reducing the chance of having defects in the tests, and it is easier to achieve greater case-coverage for pure functions as there are no side-effects to worry about.
In the vast majority of languages, writing pure functions is an exercise left to the programmer, but some functional languages will enforce it. The latter are called pure functional languages, and to my knowledge, there aren’t that many of those.
Using strictly pure functions can, in some cases, cause more bugs, however. Some operations (e.g., writing to a file or drawing on a display) are inherently impure, and are easier to express using impure functions in most programming languages. Trying to purify inherently impure operations in an impure language usually results in increased complexity, and, therefore, more defects.
Languages with guarantees
You have probably noticed that some of the techniques are related to, or even are, language features.
Static typing for example, is a proper language feature that we cannot even simulate in a language that doesn’t have it. Other techniques can be simulated, but are still much easier to take advantage of in languages that have direct support (immutability, guards, assertions). We are, therefore, able to talk about languages which inherently provide more guarantees about our code than others.
Whether using one language or another leads to more defect-free code is arguable. There are bug density statistics out there that provide a different view at the topic. There is also something to be said about the level of expertise in the language’s community, and the common application of the languages (in other words, whether it’s the right tool for the job).
Investing in a language that provides more safety is still a worthwhile goal, and there does seem to be at least some correlation between defect density and a good balance in language features.
Conclusion
We’ve looked at seven kinds of guarantees we can provide about our code. Even with all seven combined, there is no guarantee that our programs will be defect-free. Why bother then? Why not settle of a handful of the more effective ones instead? In reality, most of us do just that, and live with the cost of increased vigilance in the face of decreased safety.
Point is, each guarantee leads of having one thing less to worry about. We can let the computer worry about some aspects of the code quality, while we focus on the fun stuff, or at least look for defects in places where computer is unable to.
Also keep in mind that this list is not conclusive by any means. There are probably more tools/methods/practices out there, and programmers will discover new ways of providing guarantees about our code in future.