Fail Fast, Build Better
Recently I am focusing more on getting to know to build better software before taking it live. I want to discuss mainly on the topic of the role of “Rapid Iteration” in software engineering and how it saves us from many bugs that we generally encounter on production. We will look at things that will crash our programs early on, so that we can have a peaceful life in production later. It generally feels so counter-intuitive to think about crashing your program while building itself, but trust me! it saves a lot of time once your code is on production – Let’s start with a very simple one!
Early Returns
Seeing the data flow through your code without running is something that I got. There are many such code paths in the below example. This is one of the code-snippet from my project called xo.
// check if xo file is there in this path
if _, err := os.Stat(xoFilePath); os.IsNotExist(err) {
panic("not a xo project. do 'xo i'")
}
// read the xo.json from the current path
xoFileBytes, _ := os.ReadFile(xoFilePath)
var xoFile types.XOFile
if err := json.Unmarshal(xoFileBytes, &xoFile); err != nil {
panic(err)
}
// check if there are some commands inside xofile
if len(xoFile.Commands) == 0 {
panic("no commands inside this project")
}
// check if the project path exists
if _, ok := xo.store.GetProjectPaths()[p]; ok {
panic("project path already exists in xo workspace")
}
// all good here!! lets add this project to workspace
if err := xo.store.AddProject(p, xoFile); err != nil {
panic(err)
}
There are many error cases which are handled before we call AddProject
at the end, which contains the main logic for this flow. This allows the program to exit the parent function as quickly as possible.
But what does it give us?
It makes our AddProject
code a lot simpler, we know that whataver arguments passed inside AddProject
method are safe and easy to write.
This also saves a lot of time in a distributed environment. Imagine an app calling 3 different services, and one of them has not yet reached the error block at the end of the handler which could’ve failed if we moved this check up in the same handler. This seems to be a lot of wasted CPU cycles, and at scale it seems to be a waste of money too!
☠️ Errors Sidestepped
- Null Reference or Undefined Errors
- Out of Bounds Errors
- Division by Zero
- Buffer overflows
- Dependency Errors (missing or erroneous ones)
Having a Strong Type System
Type systems in general are very deep topic and widely researched through Type Theory. Many of the day-to-day terms that we use like class (set theory), type inference etc..
are derived from type theory. We don’t need to know what this theory means but we must
ensure that our systems are built on the foundation of concrete types known at compile-time. The type system are the ones that makes or breaks a programming language.
Any system which is developed without concrete types is what I call a fragile
system. They are quick to build, easier to wrap your head around, but they are not robust in nature. You often have to run your debugger to know what type of data is flowing through your code paths which eats your time like a termite.
Are you the guy who forces teams to adopt Typescript?
Nah.. maybe? Getting to know what types are expected upfront while coding helps you eliminate all kinds of bugs, the types in your system constitute as a layer of protection from unknown bugs at runtime.
☠️ Errors Sidestepped
- Type Mismatch Errors
- Memory Corruption / Unsafe Memory Access Errors
- Unwanted Modifcation of Immutable Data
- Incorrect Function Arguments
- Incorrect Type Casting
- Thread Safety Issues
The Big Villain of Type System
If your code compiles, it works! or does it?
I consider the Any
type the biggest villain in any language. It’s a quick fix for laziness—just replace all types with Any
, typecast it back to your desired type, and it works… at least for now.
The use of an Any
type (or a similar concept, such as Object in JavaScript or void* in C) in programming languages can introduce a range of problems, particularly when the Any
type is used extensively or improperly. While it offers flexibility by allowing any kind of value to be assigned, it can lead to several issues related to type safety, code maintainability, readability, and debugging.
I’ve dedicated an entire section to the Any
type to strongly advise against using it altogether. Let me point out the “Errors Created” not the “Errors Sidestepped” for this tiny little type!
☠️ Errors Created
- Loss of Type Safety – Duh!
- Increased Risk of Runtime Errors
- Low Maintainablity goes for a toss!
- Less Readability
- Tooling cannot save you anymore!
Fuzz it!
Fuzz testing is a powerful tool for discovering a wide range of software errors that traditional testing methods might miss. By providing random or malformed inputs, fuzzing can identify critical issues such as memory corruption, input validation errors, security vulnerabilities, crashes, and performance problems. It is particularly effective in improving the security, stability, and robustness of applications by testing how they behave in the presence of unexpected, potentially malicious input. If you want to get to know more about Fuzz testing, I’ll add some references for you in the 🍪 Brownie section below.
☠️ Errors Sidestepped
- Uncaught Exceptions and Crashes
- Buffer Overflows and Underflows
- Unresponsiveness of Service
- Resource Exhaustion (Testing the limits of your parameters)
- Parsing Errors
- Finds Missing Edge Cases
- Holes in your Error Handling
- Slow Behaviour (sometimes Denial of Service too!)
🎬 Final Take
Out of the above 24
variants of errors that I have mentioned above, there is one more thing that can create more than 1000
bugs, it’s – Our Thinking
We can make all the cool abstractions that we want and make us write code in a single line but what we want to think is,
is that much code necessary for doing this stuff?
Remember that each line of code is one (or < 1) more instruction for the CPU to run and that one line might be that which crashes your program hard!
Less Code = Less Bugs = Less Debugging = Less Time = Less CPU = Less Memory = Less Everything!
This is what the programming world is converging towards, we see many new strides made on this front, where we are catching all the above bugs – and more! before we even push our code to production.