DEV Community

Matt Eland
Matt Eland Subscriber

Posted on • Edited on • Originally published at killalldefects.com

Making Defects Impossible

Software bugs are bad, but repeated bugs of the same type can be beyond frustrating. How many times have we seen error messages containing strings like “Object reference not set to an instance of an object”? As software engineers, we can fight one-off occurrences as we find them, or we can aggressively look to eliminate common causes of defects as we identify them.

Whenever you see a defect, ask yourself how it was possible for that defect to exist, for it to remain undetected for however long it did, and what you can do to either eliminate the possibility of future defects like it or make it impossible for them to hide.

Certainly we can’t eliminate all types of issues, but the types of issues we can strategically address at the design or language level is growing every year.

This article is written from the perspective of a .NET and JavaScript development manager, but the techniques may also be more broadly applicable to other languages.

Identifying Run-Time Errors at Compile Time

Starting with an easy and fairly obvious one, compiled languages give you the ability to catch what would be a run-time error at compile time. In JavaScript, you can easily write a function like this:

function myMethod(a, b, c) {}
Enter fullscreen mode Exit fullscreen mode

And try to invoke it via:

mymethod(1, 2, 3);
Enter fullscreen mode Exit fullscreen mode

The JavaScript will parse fine but create a runtime exception when mymethod cannot be found.

TypeScript will catch this at time of compilation (transpilation, rather) preventing you from making this mistake. Additionally, TypeScript gives you static type checking via syntax like

public myMethod(a: number, b: number, c: number): void {}
Enter fullscreen mode Exit fullscreen mode

This will catch issues where you try to invoke it like

myMethod(1, 2, '3');
Enter fullscreen mode Exit fullscreen mode

Of course, this takes away some of the advantages of dynamically typed languages, but you can mix strongly typed definitions and more generic JavaScript in TypeScript. Additionally, even statically typed compiled languages have dynamic language capabilities, such as the dynamic keyword in .NET.

Ensuring Validity with Immutability

In programming immutability refers to an object’s state being unchangeable. This restriction can have some performance benefits, but the quality benefits it offers are sometimes overlooked.

Take the .NET DateTime object, for example. If you try to create a new DateTime instance representing January 35th or some other invalid date, the constructor will throw an exception. The DateTime object is designed in such a way that if you have an instance, you know it represents a valid date and have no need to do any verification to it.

The tradeoff of this, is that you can’t take an instance representing January 28th and modify the Day property to be the 35th since the date it represents is immutable. If you do want to advance a day, for example, you call a method to add a TimeSpan to the DateTime instance and this creates a new DateTime instance that is also known to be in a good state (advancing the month and year as needed).

By adopting this technique in your own classes, you can offer the same sort of quality benefits to your code. This is an approach commonly supported by functional languages such as F#.

ImmutableJS is a very well known library that offers immutability in JavaScript.

Baking Validation into Types with Discriminated Unions

Both F# and TypeScript have a concept called a Discriminated Union. A Discriminated Union is essentially the concept of an “or” type saying that something is one of a number of different possibilities.

The classical example in TypeScript of this reads as follows:

Type User = AnonymousUser | AuthenticatedUser;
Enter fullscreen mode Exit fullscreen mode

This lets you declare return types, properties, and parameters as User meaning that they can either be an AnonymousUser or an AuthenticatedUser. If you have some logic that explicitly requires an AuthenticatedUser you can call a method with a signature similar to authenticate(user: AnonymousUser): AuthenticatedUser to convert the user to an AuthenticatedUser and then require certain methods take in an AuthenticatedUser instance. This bakes validation into your typing system.

The downside of this approach is that you can have an explosion of nearly identical types and need to maintain more code for type transitions.
In the .NET ecosystem, you can use F#’s Discriminated Union feature support or use a library like OneOf to introduce the capability using .NET Generics syntax.

Null Reference Exceptions

Ask almost anyone in a .NET development shop (or potentially their customers) and they’ve seen the dreaded “Object reference not set to an instance of an object” error message.

This is a common problem in object oriented languages. By defining reference variables, it’s possible to set the reference to null.
Take the following example:

var myObject = someList.FirstOrDefault(o => o.Id == 42);
Enter fullscreen mode Exit fullscreen mode

If an object with an Id property of 42 is in someList, myObject will now hold a reference to it and calling myObject.DoSomething(); will work, but if no object exists in someList with an Id of 42, then myObject will be null and you can’t invoke a method on a null instance so a null reference exception is thrown.

Functional Programming languages get around this via a concept of Options. Options can either be of Some and None with Some representing a non-null entity and None representing a null entity.

So, what’s the difference between this and standard references in object oriented languages, or even nullable types with HasValue and Value methods? The key difference is that you can do things like this:

Option<MyClass> myObject = FindInCollection(myList, 42);

int i = myObject.Some(val => val.MyIntegerProperty)
                .None(-1);
Enter fullscreen mode Exit fullscreen mode

This makes interacting with null values explicit and forces the developer to consider null and non-null scenarios.

The above sample uses the .NET Language-Ext library for functional programming. In TypeScript you could use the fp-ts library which offers a simple set of functional programming constructs including Options. See my article on Options in Language-Ext for more details.


Ultimately, there are a number of ways to attack common programming problems. This list barely scratches the surface and I could write another article entirely on Reactive Programming and the problems it can solve, but hopefully this gives you a tip of the iceberg insight into the types of problems you can eliminate via carefully applying tools, languages, and libraries.

Bear in mind that many of these techniques have tradeoffs in readability or other facets (particularly those related to functional programming), so choosing to go with them should not be automatic, but rather a careful decision made based on the skill level and familiarity of your team members, the state of the codebase, and the nature of the types of problems you’re trying to solve.

Top comments (1)

Collapse
 
aschwin profile image
Aschwin Wesselius

Great explanations Matt! I learned a lot from this post.

Thanks!