DEV Community

Cover image for Is “Defensive Programming” actually healthy?
Cubicle Buddha
Cubicle Buddha

Posted on • Originally published at cubiclebuddha.com on

 

Is “Defensive Programming” actually healthy?

I can’t solve this one, and I think I need the help of the DEV Community. So, a developer was responding to a code review comment I made and they simply asked me, “why would I do that?” I gave my standard, dusty answer: “because you have to code defensively— you don’t know what the future holds.” But I suddenly realized… am I proliferating a fear of the future? How could I code fearfully when I run CubicleBuddha.com where I blog so often about living happily in the present? I’ll share the specific code example with you. I’m hoping to hear from the community whether my solution is “coding in the moment” or if I am actually bowing down to the fear.

A classic defensive programming example

Part of the duty of reviewing a coworkers code is to try and see what they might have missed. This follows the standard definition of defensive programming:

Defensive programming is when a programmer anticipates problems and writes code to deal with them. (1)

So, imagine you were reviewing a pull request and the code was making some assumptions. At first glance, the code sample below looks innocuous. And maybe it is. But having spent decades fixing other people’s production bugs, my spider-sense was tingly with fear. A specific bug comes to mind (which I’ll be demonstrating it in the second coding sample below) which leaves me staring at the Github code review not knowing how to proceed. I’m trapped wondering if I should keep quiet to preserve a carefree relationship with my peer or if I should speak up and prevent the potential production bug. Am I being haunted by the early years of my career where I was relegated to only bug fixing? Or were my formative years an invaluable training ground that makes me who I am today?

“If you are caught in sorrow and regret about the past, or if you are anxious about what will happen to you in the future, then you are not really free to enjoy the many wonders of life that are available in the here and now.”

~ Thich Nhat Hanh

See for yourself if you can find where a bug can easily manifest. If you can’t see the bug, then I’m almost jealous that your past didn’t inform you of the potential nightmare. There is a bliss in not knowing. But sadly, users who experience production bugs don't care about your "bliss," they just want to finish what they were doing:

Okay, yea. No problems “in the present.” And one could argue (as my peer continues to do so) that since our program is only used in geographical regions that are limited to the three major traffic signals (red, yellow, and green) that we don’t have to worry about this right now. My peer is using one of my favorite phrases against me: “You Ain’t Gonna Need It” (YAGNI). And I get it. But do we really not care about expanding the software?

And this is the biggest internal conflict I struggle with between my coding style and my philosophical beliefs. Why build software if you don’t want it to be used by an expanding group of people? There’s no shame in hobbyist programming. But if you’re a professional programmer, you’re doing it to make money and/or to improve the lives of your customers.

So, can we be pragmatic? Can we try to be a buddha in a setting so sterile as a cubicle? Can we have one foot in commerce with another foot in calmness? The coding technique below will (in my opinion) help you to make way for the future while calmly focusing on the present.

Seeing the car crash of the future… and remaining calm

So consider the fact that when you get new users, you should hopefully be learning about the needs of your new customers. And new use cases means new features to write. And here’s the classic example. Today, we only deal with 3 lights. But what if we start selling the software in other states? For instance, the state that I live in has a blinking red light where you’re required to stop first before you go (kind of like a stop sign). Let’s see if the code that worked before has protected us from the future– can you spot the calamity that would occur?

Hold on a second, if the driver saw a red blinking light… wouldn’t that fall into the fall-through/else case? Wouldn’t they… oh no! Kaboom!!! Let’s see if we can prevent that future car crash but without having to do too much more work in the present.

Defending the future: the “never” type comes to the rescue!

Thankfully TypeScript has a language feature called the “never” type that allows the compiler to identify when every case in a union of types (or every case of an enum) has not been accounted for. As you can see below, by not allowing the series of if-elses to fall through to a default else, the compiler will tell us that we forgot to instruct the driver how to respond to the “red blinking light.”

And now the driver won’t get into a car crash when we decide to start handling blinking red lights… because we literally couldn’t compile the code until we instructed the driver how to respond to this new case. In the original example, the code would have told the driver to just “go.” That doesn’t seem mindful to me.

The beauty of this defensive programming technique is that it costs almost no time to add exhaustive type checking to your code. The experienced programmer part of my brain knows that coding defensively is the simplest and best way to look out for the user’s needs. But, I worry sometimes that my career prevents me from truly acting like a Buddhist. Hopefully techniques like this “assert never” approach will allow me to strike a balance. After all, I’m just human. And Buddhism teaches us to love our humanity and to accept our emotions.


But what do you think? I’d love to hear your thoughts on Twitter and Dev.to about your thoughts on the healthiness of defensive programming. Do you think it’s worrying too much about the future? Should you only concentrate on what the software needs to do today? Or do you think it’s okay to code defensively?

Latest comments (124)

Collapse
 
vladimirpavelka profile image
Vladi Pavelka • Edited

I would argue you are using the term "defensive programming" wrongly. To me it's "offensive programming"/"fail-fast", what's described here. The original version - continue running in the else branch a flawed process and pretend everything is fine, if (null) return null, etc is the defensive programming trademark

Collapse
 
cyberhck profile image
Nishchal Gautam

I generally avoid using else, in this case I'd have used switch and on default I'd have thrown an error. But never approach is better, I like your implementation

Collapse
 
sebbdk profile image
Sebastian Vargr

Lack of foresight is unfortunately not a bug.

And objectively speaking, adding more to the solution would add complexity that only might' be needed, or at least is not needed now.

In my experience these types of debates often waste more time/resources then simply changing it later when it becomes relevant would have.

(Tho IMHO in this case it just comes off a bit lazy to not add the suggestion, if we can say with high probability that we would have more signal types in the future.)

Collapse
 
andreasjakof profile image
Andreas Jakof

When I looked at the code and saw, that „redblinking“ will result in „go“, I immediately thought:“Oh F**k!“
I like your approach, but wouldn’t a switch be more readable? Making the „nevergonnagethere“ the „default“ of the switch?

Collapse
 
seveneye profile image
seveneye • Edited

i think it depends on how complex it will be when you try to be "defensive". There are times that the code gets too complex to understand because we're trying to handle a lot of cases, when it is just a simple feature.

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

Simple features have a habit of becoming complex features as time progresses and the application grows. That’s why defensive programming provides a foundation so you can grow the program without worrying.

Collapse
 
seveneye profile image
seveneye

It takes experience to determine which simple features have the potential to be a complex one. If the code gets too complex and I'm spendin too much time, I will always revert to KISS and YAGNI and not be paranoid about future use cases because it will show up anyway and we need to do some CR/Bug/Enhancement about it. I focus more on good design so that code can be easily refactored.

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha

If you’re waiting til someone makes a bug report, then you’re waiting too long. That’s by definition reactive. The approach in this article is proactive.

The best bug is the one that never makes it to production.

Thread Thread
 
seveneye profile image
seveneye • Edited

yeah, tho i still think it should be a balance, you can never be too proactive because it might be costing you too much time, when you can just design it better. If you are already good at it then that's awesome.

In your example, in order to create add new traffic signal, you don't file a bug. You need enhancement for that and you can refactor the code the way you did. But in other cases, simple code that only handles 1 case but made too complex will be a maintenance nightmare for you and your team.

Collapse
 
aminmansuri profile image
hidden_dude

I have to agree that if there's ANY chance that a stray value is going to get into your function you should throw an exception (if your type system forbids it then you wouldn't need it.. but....).

Now about your specific example. If/else ifs/.. are a bit of a code smell in the OO era. That is why people propose the dictionary solution as a table based solution to the problem. Or one could use an OO based solution where each type is an object that responds with a method that indicates the desired behavior.

Doing it that way the OO hierarchy (or interface) would force you into doing it right every time.

But barring the specific example, defensiveness is essential. Especially in the new security conscious word.

If someone throws YAGNI at you, throw the 5 C's of programming:

Clear
Concise
Correct
Complete

and the most important these days: C-secure

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

You mention that if statements are a code smell... but what does it smell of?

Like the code in the article does it’s job and it communicates it’s intention clearly.

Collapse
 
aminmansuri profile image
hidden_dude • Edited

They are an OO code smell.. In OO things should be solved by dynamic dispatch and not by successive ifs since all the rules about a type should be in a class, rather than dispersed all over the program.

A second best option is to use tables (as others suggested) but that's just a fancy if/elseif/...

From a purist OO perspective the solution is to make methods in each type that would solve your problem.

So instead of:

if (type == 'red') {
doSomething1();
else if (type == 'blue') {
doSomething2();
} else if ...

You can just write:

type.doSomething()

Where doSomething() is overloaded for each type.
Now you may use a single if/else if/.. statement or table in one place to convert your string into an object (a factory method) but thats in only one single place in the program.

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha

If you can’t tell me why it smells (as in you can’t tell me what type of bug will occur based off of what you’re seeing) then you might be blindly following doctrine.

And as far as “the OO era” that you mentioned, many of us are moving to more functional concepts like splitting data from logic. I do that not because I want to follow the functional programming doctrine but because I found code to be more testable that way. I’ve also found that I was able to utilize composition much easier when I started to throw away the idea of encapsulation.

Consider checking out this incredible article: medium.com/@cscalfani/goodbye-obje...

Thread Thread
 
aminmansuri profile image
hidden_dude • Edited

The specific bug is that by spreading your type logic all over the program, if you need to update it, you need to find all those if statements to update them. So your code is more error prone and less maintainable. Its also a violation of the DRY principle. Specifically if you want to add a new type, and forget to update one of your many if statements you'll have a bug. Or if you want to change the behavior of a type, and forget to update it in one of the many if statements.

I did mention "OO era" in case because I new the functional style would be mentioned. Note however that with a functional style, you shouldn't throw away encapsulation necessarily. Encapsulation is modeled in your code module. You could do the exact same thing in a functional style without proliferating your code with if statements.

The functional equivalent for this is multimethods. (though I'm not sure the language you are using supports that construct)

See clojure.org/reference/multimethods for example

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha • Edited

You might be missing the point when you say this. Based off what you said, I feel that I might not have explained myself well:

The specific bug is that by spreading your type logic all over the program, if you need to update it, you need to find all those if statements to update them.

I’d like to clarify that the assertNever function tells me if a new type was added and it tells me if one was removed.

So I don’t need to “find those if statements” because the compiler will inform me.

Thread Thread
 
aminmansuri profile image
hidden_dude

That is a work around.

But no, the compiler won't tell you. An exception will tell you at runtime. Assuming you tested correctly you may find this before it hits production. But its hard to argue that this is better than just using better coding practices.

if/elseif/... is a code smell. A bad practice if it can be avoided. In this case it can be avoided.

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha

Try compiling this code. The compiler will in fact tell you if you’re missing a case that’s described in the discriminated union.

Thread Thread
 
aminmansuri profile image
hidden_dude

Of course, that only works if you recompile all your libraries and dependencies. The OO solution would work even if you only recompiled the class.

Collapse
 
joshcheek profile image
Josh Cheek

I would absolutely code it the way you did. I wouldn't say I program defensively, I'd say I program haphazardly, but one of the reasons is because I trust myself to have written code that will explode when I break something. If you don't write code like this, then when you go to add that next traffic light status, you're going to be extremely scared and want to do extensive regression testing. Plus, it's just logically incorrect to use an else here. There are a fixed number of cases, and "go" is the correct answer for one of those cases, not the default with a few exceptions.

Collapse
 
kephas profile image
Nowhere Man

The first code is indeed brittle, in the sense that it translates three definite cases as two cases and everything else, which is semantically very different.

What you achieve manually with the fourth case is what the Elm compiler does automatically with a pattern matching on an algebraic data type:

type TrafficLight = Red | Yellow | Green
type Signal = Stop | Pause | Go

respondToTrafficLight : TrafficLight -> Signal
respondToTrafficLight light =
  case light of
    Red -> Stop
    Yellow -> Pause
    Green -> Go

With this code, you have your never case for free. If someone adds a type constructor in TrafficLight, respondToTrafficLight won't compile anymore. Elm functions must be total functions.

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

Oh yea I would love to use Elm! I asked my coworkers to check it out and they almost unanimously said no. :(

Collapse
 
imtollu profile image
Ilja Tollü • Edited

That's not a defensive programming. That's just a common sense. Traffic lights are not equal. Green allows some dangerous action. Red is safe. So, it is just correct to explicitly check conditions to allow something dangerous and then fallback to less dangerous cases. Some code that would communicate this semantics would make a great job for its readers.

Collapse
 
faithfinder profile image
Dmitrii Kartashev

In this particular example, I would probably just make "stop" a fall-through case, so no matter what happens the movement doesn't start at the wrong time and call that a day. Globally, would depend on the real case, but I'd say you might want to include your reasoning in the code review from the beginning, and "you never know what might happen in the future" is not good enough. The better review would say exactly what kind of trouble you expect.

Collapse
 
jinfoo profile image
Jin Foo

This is good (defensive) programming; what differentiates this to a YAGNI issue, is - the time taken to prevent calamity is a lot less than the time needed to fix it later.

If prevention becomes bigger than the actual problem being solved then definitely, YAGNI.

In the spirit of your article, I'm going to say - this is where past, present and future meet in the eternal now, and leave you with this quote:

“To dwell in the here and now does not mean you never think about the past or responsibly plan for the future. The idea is simply not to allow yourself to get lost in regrets about the past or worries about the future. If you are firmly grounded in the present moment, the past can be an object of inquiry, the object of your mindfulness and concentration. You can attain many insights by looking into the past. But you are still grounded in the present moment.”

― Thich Nhat Hanh, The Art of Power

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

I totally agree about the preventing calamity aspect. It’s hard to straddle the line between future proofing and letting the future rule you. But I think this example is an easy decision due to the low cost and high value.

Btw I’m happy to see another Thich Nhat Hanh fan on dev.to. :) Thank you for your excellent reply. :)

Collapse
 
sebbdk profile image
Sebastian Vargr

A PR should be merged if it solves a problem and does not break/prevent future changes.

Then you can always do a followup PR to fix styling, complexity etc.

Everything else can be solved outside of the PR.

Need better styling? Teach people this.
Need less complexity? Pair program and mentor people.
Need people to see future problems? Share your code visions with the team at a planning meeting or even better write a spec’.

Mind, it took me a while to get over my own ego to realize this... :)

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

Yup, I totally agree that if you want to raise the level of expertise on the team it has to come from mentoring. But it’s kinda important to mention that:

  • some people don’t want to pair program
  • on remote teams the code review is that primary mentoring opportunity

So while I love pair programming personally, sometimes I have to use the code review as my chance to learn and to teach.

Collapse
 
sebbdk profile image
Sebastian Vargr

I was speaking in broad terms, I totally agree, this is not a one shoe fits all situation. :)

Both of those examples however present a less than ideal situation, that could be resolved outside of the PR.

Such as, keep people that need to learn on site, and not remote. And to teach people the importance or pair programming, especially in a learning setting.

If these can’t be done, then the mitigating action can be to use the PR.

But it really should be considered last option, as it is super unproductive, compared to alternatives. At least in my humble experience.

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha

“keep people that need to learn onsite”

I work at a 100% remote company, so there is no “onsite” location

“teach people the importance of pair programming”

Good idea! In fact I tried that today. I’m on a pair programming break right now. :) It’s tough going at first, but I’ll be sharing my learnings on it in a future article.

Thank you for the fun conversation. I hope you subscribe/ keep reading. I like having readers who challenge my assumptions! :)

Collapse
 
nancycantu profile image
nano-c

I think if you're working on anything that's got security or safety implications, or that is production-critical, this is mandatory.

Collapse
 
dvddpl profile image
Davide de Paolis

Interesting article ( and very interesting blog btw :-) )
I don't really like the word defensive programming because over the years I see too many bloated methods (even in typed languages like C# and Actionscript) full of null checks, undefined checks for basically everything. and every time I asked " but why... ??" the response was always " Just to be sure " or " better be safe than sorry" or " do you really wanna risk a crash on production !?!?"(the last being pronounced with a horrified face showing disappointment for my being so irresponsible).

But I agree that it is better to make the best out of our coding style and out of the tools we have ( strictly type languages, linters, unit tests) to prevent errors that might happen in the future.
The first thing that I thought when I saw your example was that i would have used a Dictionary/Map to retrieve the right action for the right signal. if there is no mapping then there would be an error. Of course, that would not work at compile time - so the solution suggested here is perfect and elegant.

But when it comes to really be defensive then I sometimes get very paranoid and think of what could happen at runtime. All your type checking works only at compile time: in fact, the elegant solution would be converted to this in simple js:

function respondToTrafficSignal(signal) {
    var mapping = {
        red: "stop",
        yellow: "pause",
        green: "go"
    };
    return mapping[signal];
}

And nothing would prevent your function to be executed passing invalid values. (imagine that the signal is a value coming from the server or from any external API )
Of course with a mapping instead of an if or switch you will get "undefined" rather then "go" but you might end up with unpredictable behaviour anyway. In such case, an error thrown ( and properly handled ) would be better.
So to conclude: as always Defensive Programming: Good? Bad? It depends :-)

Collapse
 
cubiclebuddha profile image
Cubicle Buddha • Edited

Thank you for your kind reply. Yes, I’ve been cooking up the blog articles for many months now. :)

So as for your code I your response, I’m a bit confused about the benefit. Your function returns undefined (as you mentioned). I don’t understand why you feel that’s a good thing. That means that a consumer code needs to either handle the undefined case every time, and if they forget to do so it will be a runtime exception. And that exception would be some kind of unclear “undefined exception.” Even in JS it would be better to throw an error so as to be explicit about the reason why it couldn’t find a match. But there’s a better way with TypeScript where you kind find out months before an error would ever be discovered. The compiler is run on every check in. Why wouldn’t you want the faster feedback of finding out when you check your code in?

Personally, I find that I go much faster if I have a short feedback loop where I can find out what I did wrong. Now, I realize that I’m arguing for static type analysis, which is not everyone’s cup of tea (although the recent Stack Overflow survey showed immense support for TypeScript). But the main point I’d like to make is that it truly is better to avoid bugs in production if you can catch them sooner.

Collapse
 
dvddpl profile image
Davide de Paolis

probably I did not articulate my reply properly.
I totally agree in the benefit of the static type checking. and I definetely want to catch error at compile time.
And i said that i would implement that check not with an IF /ELSE IF nor with a SWITCH, rather with a Mapping like suggested above by @jvanbruegge :

type TrafficLight = "red" | "yellow" | "green";
type TrafficAction = "stop" | "go" | "pause";

type TrafficResponse = {
    [k in TrafficLight]: TrafficAction;
};

function respondToTrafficSignal(signal: TrafficLight): TrafficAction {
    const mapping: TrafficResponse = {
        red: "stop",
        yellow: "pause",
        green: "go"
    };
    return mapping[signal];
}

BUT i would also be even more defensive and check for possible error at runtime in case - at runtime the function is called with an invalid value.
At runtime Typescript does not exist, after you compile all your type checking is gone and what you have is the function I posted.

function respondToTrafficSignal(signal) {
    var mapping = {
        red: "stop",
        yellow: "pause",
        green: "go"
    };
    return mapping[signal];
}

See and play around with this Typescript Playground snippet for comparison
Therefore, if the invocation of the function could be dynamic ( server could respond with blue or user could type in orange ) i would add a catch and a fallback to prevent a runtime error.

Hope i was clearer now :-)

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha • Edited

Yup yup. Yea the map + the undefined check would be the ideal approach. Sorry I didn’t understand at first. Classic misunderstanding with remote communication. My bad! :)

Oh and as far as the server sending bad or new data types, I have been using a library called TSOA to enforce runtime types at the boundaries. It’s a way of preventing “garbage in garbage out.” It’s pretty cool stuff. There are similar libraries that do runtime checking in the UI too.

Thread Thread
 
jvanbruegge profile image
Jan van Brügge

No, I would not add runtime checks to this. If you call the function with something different than the types specify that's on your own. The caller has to verify that he can call the function

Collapse
 
drm317 profile image
Daniel Marlow

Every professional software deliverer has the responsibility to consider the implications of what they’re doing and when.

Defensive programming is one example where this comes up.

This post is really about how we balance the relative costs of doing or not doing something at a particular point in time. The bad news is that YAGNI is a little more complicated than the immediate “cost to build” the unnecessary feature you’ve described.

What if your customer had another, much more valuable feature for you in the meantime? You might of incurred a “cost of delay” in not giving the customer that other new feature quickly.

In introducing the presumptive feature now you’ve also incurred a “cost to carry” that code around in your codebase. You’re going have to ask yourself and others to read it and understand it, to store it and version control it.

When you do eventually encounter the breaking scenario you’ve described in the future you will incur a “cost to repair” that feature. That cost might be greater than it would be now if your code changes a great deal in the meantime.

So YAGNI absolutely should not apply to refactoring that allows you to change your code easily. A clean codebase is the ultimate defence kit against these costs.

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

I really like your thought process on this. Though I just want to make sure I understand you correctly: Are you saying that YAGNI is not a reasonable argument against clean code techniques like the never/exhaustiveness checking I showed above?

If so, I agree with you because it helps to keep the codebase clean, and as you said:

A clean codebase is the ultimate defence kit against these costs.

Well put. :)

Collapse
 
drm317 profile image
Daniel Marlow

Yes. As Martin Fowler puts it:

“Yagni only applies to capabilities built into the software to support a presumptive feature, it does not apply to effort to make the software easier to modify.”

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha

Wow. That’s an incredible quote. Thank you so much for making me aware of it. :)

Thread Thread
 
drm317 profile image
Daniel Marlow

Here’s the reference along with a discussion of the cost considerations behind YAGNI

martinfowler.com/bliki/Yagni.html

Why You Need to Study Javascript Fundamentals

The harsh reality for JS Developers: If you don't study the fundamentals, you'll be just another “Coder”. Top learnings on how to get to the mid/senior level faster as a JavaScript developer by Dragos Nedelcu.