Skip to main content

Command Palette

Search for a command to run...

Defensive programming.

We'll learn from mistakes, or so they say. Instead of waiting for that moment of regret, why not put out measures first-hand before hitting "release".

Updated
19 min read
Defensive programming.
J

I am a full stack developer from Uganda, author of jet-fetch, and creator of Pionia Framework.

Let’s address the elephant in the room. We've all encountered bugs that seem to appear out of nowhere; sometimes, we've faced security breaches, and other times, we've crashed the server, much to the annoyance of our boss (which was amusing until it wasn't, especially when we couldn't fix the issue). We've all been there. The unfortunate part? We keep falling into the same trap, merely covering up the problem without fully resolving it.

I understand this is a broad topic, so I'll cover what I can to inspire you for your next exciting project, helping you avoid the pitfalls I once encountered. The examples in this discussion will primarily involve four tools: Java, JavaScript, and some in Python and PHP.

  1. Fail first.

Let’s start with this first mechanism. Consider a scenario where a specific block of code (method) performs four tasks: it logs the request to the database, logs to an external tool like Slack, retrieves the user from the database, and finally caches the obtained user. However, to accomplish this, it relies on an id as an input. Let’s examine this:

Non-defensive version

private function getUser($id) {
    # Log the request to the database
    # log to an external source like Slack
    $checkCached = Cache::get('users', $id) // 
    if($checkCached){
        return $checkCached;
    }

    $user = User($id)->get();
    Cache::set('users', $id, $user, 10);
    return $user;
}

Let’s break down what we just did:

  • We log the request in our local database.

  • Then, we log the same request to an external platform — perhaps an analytics tool or Slack.

  • We check if the user is in our cache; if so, we return that user.

  • Otherwise, we retrieve the user from the database using the given id and cache it for later, maybe for 10 minutes.

  • Finally, we return the user.

Let’s pause for a moment. What happens if we don't have an id or if it was passed as null? All the tasks above depend on the availability of the id; otherwise, we should abort immediately. Imagine completing the logging process only to realise the user did not provide the id! This is why verifying the existence of the id is crucial and should be done first.

Defensive version

private function getUser($id) {
    if(!$id || !is_int($id) || $id < 0){
        throw new Exception("getUser:- Invalid ID provided ". $id);
    }
    # Log the request to the database
    # log to an external source like Slack
    $checkCached = Cache::get('users', $id) // 
    if($checkCached){
        return $checkCached;
    }

    $user = User($id)->get();
    Cache::set('users', $id, $user, 10);
    return $user;
}

The real magic happens with the condition below. By checking this first, we dodge those pesky null pointer exceptions and ensure we only move forward when all the necessary data is in place.

if(!$id || !is_int($id) || $id < 0){
    throw new Exception("getUser:- Invalid ID provided ". $id);
 }
  1. Fail Loud.

We'll derive this from our first step. There's not much to add here, so let's dive into the problem and explore how we can apply defensive programming.

Non-defensive mechanism

if(!$id || !is_int($id) || $id < 0){
    return null;
 }

Here, we're failing in a rather awkward way by returning nothing, leaving frontend developers scratching their heads over what caused the null. 🤔 You've never seen a frontend dev question their life choices like this! They'll even start blaming the desk they're working on. Please, spare them the existential crisis! 😅

Defensive mechanism

if(!$id || !is_int($id) || $id < 0){
    throw new Exception("getUser:- Invalid ID provided ". $id);
 }

I recommend raising exceptions and handling them globally, instead of returning nothing.

  1. Default if you can.

This is a "fail-safe" option. Here, you're essentially saying, "You forgot it, but I've got your back." Instead of dealing with null or undefined, you can provide a default value to use if the user hasn't specified one.

const activateUser = (status = true) => {
    ContextUser.setStatus(status);
}

This will ensure the status will always be true if nothing is passed while just calling activateUser().

  1. Dictate the appearance.

This is an option you only have with some languages, but not all. We normally get this with a concept of Enums. Let’s take the example of Java, where we update the order status. This can be done on a happy, innocent day.

Non-defensive

// all we need here 
public OrderController {
    public Order updateOrder(Order order, String status) {
        order.setStatus(status);
        return orderRepository.save(order);
    }
}

Now, let’s consider the potential issues. Imagine someone sets the status to something like awesome or Java; our current program would simply save it without any validation.

Defensively

We can come up with a Java enum of our statuses that shall ensure a valid status was passed/provided. With this approach, our backend is very much aware of all the statuses there can be.

enum OrderStatus{
    PENDING;
    APPROVED;
    SHIPPED;
    DELIVERED;
    RETURNED;
}

public OrderController {
    public Order updateOrder(Order order, OrderStatus status){
        order.setStatus(status.name())
        return orderRepository.save(order)
    }
}

Here the entire backend knows all the statuses an order can ever be in. If the frontend passes anything else, and exception will be raised. Our updateOrder method nolonger takes just any string, but a string which conforms to our enum OrderStatus

  1. You did not create it, don’t trust it — optional chaining

Some of us have trust issues, and sometimes this is a good thing. Personally, if I did not write the API, even if my superior did, I do not trust it. Occasionally, I find it hard to trust what I have written on the back while trying to consume it on the frontend. This is a protective trait and an essential attitude to adopt. Don’t be misled by TypeScript's strict typing; at the time of bundling, it all reverts to JavaScript, which is a clever watchman without any weapon.

Scenario one

Let’s explore what typically occurs in this situation. Imagine consuming an API like this.

Non-defensive

// get a user by their ID, otherwise, return the last inserted user
export const getUserById = async (id = null) => {
    const query = id ?? ' latest'


    const response = await ourGetFetchHelper(`/endpoint/${query}`);
    //This is the point where you have over-trusted someone else. 
    setUserNameToState(response.user.username);
}

So, what happens when the response is undefined, the user isn't found, or an exception is raised? 🤔**
Second scenario**

The above is a straightforward scenario, but imagine dealing with a massive transactions JSON where you're relying on the transaction status to set the card color, and then—boom!—one transaction arrives without a status! 🎨 Or consider when you're storing user data in cache, like local storage or a Realm database for mobile, and using it to display the user session in the navbar or home screen with a full name that's a combination of first_name, last_name, and username. Meanwhile, user Kapele Lugard forgets his first name, only providing "Lugard" and a catchy username like "lugard@256." 🤔

Let’s look at this scenario, too.

const login = async (username, password) => {
    // we assume and are sure that username and password will be available at this point.

    const response = await yourPostFetchHelper('/login', {username, password});

    setUserToGlobalStateManager(response.data);
}

// somewhere in our app we want to get the full_name
const user = getUserFromGlobalState() //-- whichever way you're accessing your storage

<div>
Welcome {user.first_name.toUpperCase() + ' ' + user.last_name.toUpperCase() + ' :: '+ user.username.toLowerCase()}
</div>

Imagine the chaos if any of first_name, last_name, or username turns out to be null! Your beautifully crafted user interface could suddenly look like a mystery novel with missing pieces! 🕵️‍♂️✨

Defensive

Optional chaining comes to our rescue. Meanwhile, it’s acceptable to write a series of if statements, but sweet watchman JavaScript, which is clever yet limited, has an intelligent way of managing this. Now, before we proceed, don’t be tempted to think that Typescript will eliminate all null pointers; after bundling and compiling into JS, if you have not conducted these checks yourself, some of those guards will be lost, leaving you exposed. Let's gear up and go defensive. 🛡️😄

// First scenario
// get a user by their ID, otherwise, return the last inserted user
export const getUserById = async (id = null) => {
    const query = id ?? ' latest'


    const response = await ourGetFetchHelper(`/endpoint/${query}`);
    //This is the point where you have over-trusted someone else. 
    if(response?.user?.username){
        setUserNameToState(response.username);
    }

    //You can also do something shorter like below
    response?.user?.username && setUserNameToState(response?.user?.username);
}

In this scenario, we ensure that the response is defined, the internal user object exists within the response, and finally, the username is present. Only then do we interact with our global state. Otherwise, we remain gracefully inactive. Also, in our UI, we only render the “welcome“ div, only if we have an authenticated user.

const login = async (username, password) => {
    // we assume and are sure that username and password will be available at this point.

    const response = await yourPostFetchHelper('/login', {username, password});

    response?.data && setUserToGlobalStateManager(response?.data);
}

// somewhere in our app we want to get the full_name
const user = getUserFromGlobalState() //-- whichever way you're accessing your storage

{user && (
    <div>
        Welcome {user?.first_name?.toUpperCase() + ' ' + user?.last_name?.toUpperCase() + ' :: '+ user?.username?.toLowerCase()}
    </div>
)}

Maybe something to add here is when you're parsing JSON.

const parseDataFromSomeKaFileToJson = (jsonString) => {
    return JSON.parse(jsonString);
}

In this case, what happens when jsonString is null or undefined? 🤔 If you hand me a null jsonString, I'm likely to think, "Well, that's exactly what I'll return to you!" Let's dive into how I'd handle this with a touch of flair. 😄

const parseDataFromSomeKaFileToJson = (jsonString = null) => {
    try{
        return JSON.parse(jsonString);
    } catch (error){
        return null;
    }
}

Now we are almost certain of the security in the parseDataFromSomeKaFileToJson method, no matter who or how they call it.

  1. Check the type, not the existence.

Let me share a real-life scenario. The mobile app was consuming an events API from a Python backend. There were two endpoints: one returned the event's details, and the other booked an event by its ID. The endpoint returning JSON was sending the following data or something close to that.

{
    eventId: "64",
    eventName: "Awesome event Name",
    ticketCategories: [
        //... data about ticket categories
    ],
    // more data, really mob here.
}

Notice how the eventId is a string. On the frontend, this wasn't an issue, but the real problem arose with the action responsible for booking the tickets, which was set up as follows.

def book_event(request):
    eventId = request.POST.get("eventId")
    if eventId is None:
        raise Exception("Event Id is not defined")

    event = get_object_or_404(Events, pk=eventId)
    # rest of the logic

# routes
path("book/", book_event, name="book_event")

Hitting this endpoint was returning a 404, even though I was sure I was sending the right ID. The check was ensuring the request data contained an eventId, so it checked for presence, and since I was sending the ID, it passed. But here's the twist: the ID is a string, while the database expects an integer. It's a bizarre and logical error. Pray you never encounter one of these! Where do you even start debugging? It's like finding a needle in a haystack, only to realise the needle was a piece of spaghetti all along! 🍝🔍😅

And don't even ask why I wasn't using query parameters—I despise them and whoever invented them! But for some reason, our eventId was being delivered through the POST request data. 🤷‍♂️📬

All said, how do we go about this?

There might be two ways of going about this.

  • Ensure the integer by conversion

  • Block non-integer from proceeding

By Conversion

def book_event(request):
    eventId = request.POST.get("eventId")
    if eventId is None:
        raise Exception("Event Id is not defined")

    eventId = int(eventId) # added this bad boy here to convert "64" to 64

    event = get_object_or_404(Events, pk=eventId)
    # rest of the logic

# routes
path("book/", book_event, name="book_event")

Now we are certain that, regardless of the string the frontend sends, we will attempt to convert it to an integer, unless otherwise specified. Thus, our “64“ will be converted to 64, which is what we want.

By Blocking

def book_event(request):
    eventId = request.POST.get("eventId")
    if eventId is None and not isinstance(eventId, int): # notice here
        raise Exception("Invalid Event ID") # and here

    event = get_object_or_404(Events, pk=eventId)
    # rest of the logic

# routes
path("book/", book_event, name="book_event")

Now we're serious—if eventId isn't provided, you won't proceed anywhere at all. This approach is better and highly recommended. It effectively prevents any unforeseen issues that might pop up in the future. 🚫🔍

  1. Conform to one sure standard — And maintain that.

We shall use two scenarios here.

The first scenario involves rendering the Pay Loan button only when the loan status is ACTIVE. However, the server might send the status as Active one day, active the next, and in various other forms.

Non-defensive.

{account.loan_status === 'Active' && (
  <Button
    label={"Pay Loan"}
    onPress={() =>
      navigation.navigate('PayLoan', {
        loan_number: account.acct_no,
      })
    }
  />
)}

With the above approach, you ensure that the status is first converted to lowercase before evaluating the condition, allowing it to pass as long as the word is "active," regardless of its case.

Defensive

{account?.loan_status?.toLowerCase() === 'active' && (
  <Button
    label={"Pay Loan"}
    onPress={() =>
      navigation.navigate('PayLoan', {
        loan_number: account.acct_no,
      })
    }
  />
)}

Now with the above, you’re sure that the status will first be converted to lower case before evaluating the condition, which will pass as long as the word is active, no matter the case.

  1. Time it right to prevent leakage.

Sometimes, background requests can take a long time and lead to issues and maybe failures. Let's imagine two scenarios:

  1. You want to perform a background task only if the user is in a specific context, like being logged in or out. Switching the context should abort all the pending background polls that were currently ongoing.

  2. A user is downloading a large file and realises midway that it's already downloaded. How do we stop this ongoing download? 🤔

For the second scenario, you might think of adding a cancel button to stop the download. But how do you actually cancel an ongoing download? 🚫

Non-Defensive Approach
Typically, a standard HTTP call doesn't handle this well. It's like trying to stop a train with a spoon! 🚂🥄

Defensive Approach
Let's add a timed request that will automatically abort after a few seconds, allowing other requests to proceed smoothly. ⏱️✨

const refetchPostById = (id) => {
    if(!id || !Number.isInteger(id)){
        // you can throw an exception here as you see fit.
        return; 
    }
    console.log('Starting request with AbortController...');

    const controller = new AbortController();
    const signal = controller.signal;

    // Start a fetch with abort signal
    fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, { signal }) 
        .then(response => response.json())
        .then(data => {
             console.log('Request finished:', data);
             // here you can even update your dom with the new post fetched. 
         }) 
        .catch(error => { 
            if (error.name === 'AbortError'){
                 console.warn('Request was aborted!');
             } else { 
                console.error('Request failed:', error);
             } 
        });

    // Automatically abort after 2 seconds
    setTimeout(() => { controller.abort(); console.log('AbortController triggered: request cancelled after timeout.'); }, 2000);
}

// for every 10 seconds, repeat the request
setInterval(() => refetchPostById(10), 10000)

Now, every 10 seconds, a new check will be initiated, which will only be given 2 seconds to run. If it exceeds that, it shall be aborted, and we shall wait for the next interval instead to clear our mess, if any.

  1. Mutate the right state.

Let’s start by looking at the example snippet below to clearly understand what might happend to us.

users = ['you', 'us']

users_copy = users

Let’s pause for a moment and imagine the output of the following now:-

users_copy.append("them")

print(users) # ['you', 'us', 'them']

Imagine you have a list in your program that you trust is safe and sound. Then, someone else comes along, thinking they're clever, and makes what they believe is a copy. They start tinkering with their version, blissfully unaware that they're actually altering your original list too! Now, picture the chaos that could ensue from this little mix-up. It's like a comedy of errors waiting to happen! 😂🔄

You have a list of workers that you manage and pay a certain amount. You ask someone to update this list by adding new workers who have joined, while keeping the original list unchanged. In your mind, the existing workers should receive an allowance, while the newcomers should only get their salary without the extra perks. But with the current approach, the developer accidentally includes the new workers in the allowance group too. And guess who ends up with unexpected expenses? Yep, take a look in the mirror! 😅💸

Here, we can look at this in two ways. The second is our point number 6. But for now, let’s look at the first one.

old_staff = ['John', "Jet"] # these are going to be getting perks
current_staff_list = users.copy() # these are going to get the salary 
current_staff_list.append("Peter") # Peter is only getting the salary not the perks
current_staff_list.append("Brian") # Brian will also only get the salary not the perks
print(current_staff_list) # ['John', 'Jet', 'Peter', 'Brian']

By using the list method copy(), we can keep our original list intact, preventing any unexpected modifications. Most programming languages that support lists, arrays, or iterators offer a way to create a copy that leaves the original unchanged. Be sure to learn how to do this in your specific language!

This mechanism has one problem: you, the creator of the original list, are not in control of the enforced creation of copies before mutating the array. How then do we proceed? Let’s go to our second approach, which is also our number 6.

  1. Lock mutation.

With this approach, you lock mutation or to put in another way, you prevent altering your list. In that case if we take the approach of two languages, that is python and Js. let’s how we can do this with Python first.

In python — we take the advantage of tuples, tuples are still lists, but once declared, they can’t be mutated at all.

old_staff = ("John", "Jet") # no one shall be able to add or remove anything in here.
current_staff_list = list(old_staff) # we create a list(mutable) from our tuple(immutable) 
current_staff_list.append("Peter") # Peter is only getting the salary not the perks
current_staff_list.append("Brian") # Brian will also only get the salary not the perks
print(current_staff_list) # ['John', 'Jet', 'Peter', 'Brian']

So, Kapele Lugard tried to change old_staff and got a clear error message: "Mutation not supported!" But Lugard, being clever, thought he could outsmart Kasolo, the mastermind behind old_staff, by creating a mutable list from Kasolo's tuple. Little did he know, he was playing right into Kasolo's hands! Now Kasolo can sit back and relax, knowing his plan worked perfectly! 😎🎉🛡️

With JavaScript — Object.freeze() to our rescue.

const users = Object.freeze([
    {
        "id": 1,
        "name": "John"
    },
    {
        "id": 2,
        "name": "Peter"
    }
])

const new_users = [...users]; // notice we create the copy here, const new_users = users won't be mutable either.

// now we can mutate new_users while the users array is very much intact.
new_users.push({
    "id": 2,
    "name": "Jet"
})

And just like that, Kasolo sets off on his trip, confident that Kapele will never mess with his list of favourite employees. He's already planning how to spend the leftover trip money on extra allowances for them! Meanwhile, Kapele is left scratching his head, wondering how Kasolo managed to outsmart him yet again. It's like Kasolo has a secret force field around his list! 😄✈️🛡️

  1. DRY — Expensive logic in one place.

We often hear this advice, but it usually stops at just being told. So, I'm here to not just tell you, but to kindly insist: don't over-repeat yourself! Doing so creates multiple points of failure. And this applies to more than just code—take it from your personal life, too. Tell your partner something once and hope it slips their mind in the courtroom of life. But if you keep repeating it, trust me, it'll come up in every argument! Now, let's get back to our coding dilemmas. 😄

What code snippet do you truly need here? Take a moment to consider why you're repeating the same functionality across 10 pages or 10 controllers instead of writing it once as a helper or utility and referencing it. When issues arise, you'll have just one place to debug, making your life much easier unless you are intentional about having a hard life!

  1. CBS - (Commit before shutting)

Why on earth would you leave your office, workstation, desk, or wherever you code (yes, even the restroom for some of you) without pushing your changes to your favourite version control tool? I've heard tales of someone who backed up his work on a hard drive, then packed both the hard drive and his PC into his backpack to head home for a nap. But guess what? A thief wasn't picky and snatched the whole bag, taking both the work and its backup! Do you really think we're all just waiting to steal your code once you push it? Meanwhile, you're busy "researching" and "reusing" ours! Push your code, even if it's just to fix a missing semicolon ‘;’! Push it or be ready to start from scratch.

Personally, when I was learning Linux, my machine used to crash more than three times in just a week. Now imagine, amidst setting up the entire OS, I was also rewriting all projects from scratch (by the way, I did rewrite projects for like the first 10 crashes since some of us learn the hard way). Don’t do it, let me be the last person to make that mistake. 😅

  1. Reach by interface, not by the implementation.

Interfaces are key in implementing polymorphism. If you did not know, polymorphism just means, “having multiple forms“. Just like a chameleon, Polymorphism makes your code change colours whenever it wants.

A polymorphic object adapts its “form” dynamically, just like your chameleon buddy changing colours to match the vibe 🦎✨.

Imagine creating a class called PaymentIntegrator and this class looks like this.

class PaymentIntegrator {

    public static function configure(...$config) {
        // initialize the gateway SDK and set it in the context. 
    }

    public function pay(float $amount, string $narration, $sender, $receiver, ...$others) {
        // make a payment using the intialized configuration    
    }
}

This setup might seem fine until you decide to handle payments using different services like Flutterwave, Stripe, or even integrate your wallets. With the current implementation, you could end up writing over five separate classes, each defining the pay and config methods independently. This can quickly become unmanageable. Now, imagine extending this approach to other functionalities like sending emails, SMS, push notifications, and more. Instead, you can create an interface that these plugins or packages adhere to, streamlining your code and making it more efficient.

class PaymentIntegrator {
    PaymentInterface $paymentSdkToUse;

    public function __construct(PaymentInteface $paymentSdkToUse) {
        $this->paymentSdkToUse = $paymentSdkToUse
    }


    public function pay(float $amount, string $narration, $sender, $receiver, ...$others) {
        // make a payment using the intialized payment gateway  
        // maybe first record the transaction locally and the start processing it on the third party.

        return $this->$paymentSdkToUse->initiatePayment([
        'amount' => $amount,
        'narration' => $narration,
        'sender' => $sender,
        'receiver' => $receiver
        ])
    }
}

With this approach, the PaymentIntegrator no longer creates and initializes the payment SDK. This is known as "loose coupling." Remember those high school relationships? You can relate! We're no longer binding a single SDK, and therefore a single payment gateway, to our PaymentIntegrator class. Anyone implementing the PaymentInterface is expected to have the InitiatePayment method, which we can call with parameters to complete the payment. This way, we're not tied to a specific implementation (tight coupling) but are using a generic interface (loose coupling). This makes your code easier to scale and modify without rewriting the exposed classes and methods.

Conclusion

In conclusion, defensive programming is a crucial practice for developers aiming to create robust, secure, and maintainable code. By anticipating potential issues and implementing strategies to handle them proactively, developers can minimise bugs, prevent security breaches, and ensure smoother user experiences. The techniques discussed, such as failing first, failing loudly, using defaults, and employing optional chaining, provide a solid foundation for writing resilient code. Additionally, embracing practices like loose coupling, avoiding code repetition, and ensuring proper version control can significantly enhance the scalability and reliability of software projects. By adopting these defensive programming strategies, developers can not only improve their code quality but also reduce the time and effort spent on debugging and maintenance, ultimately leading to more successful and efficient projects.

Let take the rest of this discussion in the comment section.