Global Variables: The Potential Landmine Lurking in Your JavaScript Codebase

Global Variables: The Potential Landmine Lurking in Your JavaScript Codebase

A solid understanding of this core concept will save you a whole lotta headache in your coding journey...

When taking online classes, watching and implementing coding tutorials, or even conversing with fellow programmers, it is so easy to delude yourself into thinking you completely understand the basic concepts of a language. Yup...it's all fun and games until your app starts acting wonky and you just can't figure out where you went wrong.

Help! My Code's Doing Magic...

So sometime last month, I scripted a bulk-upload feature on the server-side for an app in development at my workplace. Tests were written, the code was reviewed, the PR merged, the feature successfully integrated on the client-side, and everyone went on their merry way.

But a couple of weeks ago, we did an internal demo for the product and the bulk-upload operation threw an unexpected error. Even worse, the error seemed to pop up at odd times, making it difficult at first to establish a pattern or determine the root cause. The feature would work as expected and then bam, suddenly throw an error.

Global Variables and Sorcery...Two Sides of the Same Coin

We were about to go live with the product, and so the error had to be fixed and quickly. But of course, to fix it, I had to figure out what was causing this seemingly random 'magical' error.

Days later and with the help of a senior developer, I had my answer. It turned out that a variable blithely declared globally in the codebase was one of the main culprits.

What's the Big Deal About Global Variables?

To understand why we must be careful when declaring variables globally, we must first understand the concept of variables.

*P.S. If you feel confident enough in your understanding of variables and don't need a refresher course, simply skip to the next sub-heading: Scoping

A Basic Intro to Variables

Variables, in layman terms, are just storage containers. We use them to store values we will need later on in our applications.

In the code snippet below, I declared a variable called number and assigned it a value of 10. This simply means I've stored 10 in number. So anytime, I need to use 10 in my application, I just call number.

const number = 10;

console.log('2 multiplied by 10 is', 2 * number);

I use Quokka.js, a VS Code extension that provides a playground for you to write your JavaScript and TypeScript codes and see them run in real-time. So as you code, you can immediately view your output and easily debug. Here's the output of the code snippet above via Quokka.

Code output

You can also declare variables without storing a value in them immediately. In the example below, I've declared the variable myArray and initialised its value to an empty array. At that point, it's an empty storage container, but a container nonetheless.

Later on in my application, when I get values from operations that I might need to use in subsequent operations or even display to my end user, I can simply store them in the myArray variable already declared.

const myArray = [];

console.log(myArray);

const productValue = 25 * 2;
const addValue = 10 + 12;

myArray.push(productValue, addValue);

console.log(myArray);

And as you can see from the output below, at first myArray was empty when we first declared it and tried logging/displaying its value. But after I pushed into it, it now contains the values of 50 and 22.

Code output

Pop Quiz: What do we call productValue and addValue?

If you said variables, you're a veritable genius! Yes, they are also variables; I declared them and then stored the results of mathematical operations in them...all in the same breath!

Little Tip: Variables in JavaScript are declared with either the const, let or var keywords. These keywords signal to the compiler (a program that converts your code into machine-level code for execution) that you're about to declare a variable. Each keyword has its own use case, but that's a story for another day.

Now let's talk about scoping.

Scoping

When it comes to variables in JavaScript, understanding the concept of scoping is invaluable knowledge. A variable's scope is determined by its location, that is, where it is declared. And that scope, in turn, determines the lifetime and accessibility of the variable.

To put it simply, where you declare your variable determines how that variable behaves, where you can use it, and how long the variable will store the value you assign to it. That, my friends, is scoping in a nutshell.

A local variable is a variable with a local scope. This means that:

  1. the variable is declared inside a function. So it is local to that function.
  2. the variable does not exist until the function is called or invoked.
  3. every time the function is called, the variable is created afresh.
  4. the variable can be used only inside that function.
  5. once the function ends, the variable no longer exists.

A global variable, on the other hand, is a variable with a global scope. This means that:

  1. the variable is declared outside of a function.
  2. it can be used anywhere in your code or application. This means that different functions can make use of a global variable.
  3. it only ceases to exist when the entire program ends or if your app is restarted.

In the snippet below, I declared two variables: one global, the other local, and then attempt to display their values at the end of the code.

const numberOne = 15; /* This is a global variable */

/* Creating a function... */
const someFunction = () => {
  const numberTwo = 20; /* This is a local variable */
  /*
    More lines of code...
  */
};

console.log(numberOne);
console.log(numberTwo);

And this is the result:

Code output

numberOne is a global variable, so it can be accessed anywhere. As such, its value will be displayed. However, because numberTwo is a local variable, it doesn't exist outside the someFunction function and the undefined error is thrown.

Let's go back into our function and try to display numberTwo in there as it only exists within the function.

const numberOne = 15; /* This is a global variable */

/* Creating a function... */
const someFunction = () => {
  const numberTwo = 20; /* This is a local variable */
  /*
    More lines of code...
  */
  console.log(numberTwo);
};

console.log(numberOne);

Looking at our output, the error has disappeared but we still can't see numberTwo displayed. This is because we are yet to call/invoke the function.

Code output

So, let's call the function in our code:

const numberOne = 15; /* This is a global variable */

/* Creating a function... */
const someFunction = () => {
  const numberTwo = 20; /* This is a local variable */
  /*
    More lines of code...
  */
  console.log(numberTwo);
};

console.log(numberOne);
someFunction();

And voila! Now we can see numberTwo displayed in our output:

Code output

If you have some free time on your hand, you can copy the code above and immediately after invoking the function at the end of the code, try logging numberTwo again. Like this:

console.log(numberOne);
someFunction();
console.log(numberTwo);

Your output would display the values of both numberOne and numberTwo as well as an undefined error. Why? Well,

  1. numberOne would be displayed because it is a global variable.
  2. because someFunction() is invoked, numberTwo would also be displayed.
  3. but the final line console.log(numberTwo) tries to log a local variable outside the function where it's declared. And of course, that's not gonna work.

If I was a betting girl, I would bet my bank balance that you're probably thinking "Local variables suck. They're just too restricted. I'm gonna declare my variables globally." But that could prove fatal in the long run, especially when building real-world applications.

Let's Create Magic - Global Variables in Action!

Let's simulate a very basic app that accepts and displays users' email addresses as long as Gmail is the email provider. Every time someone uses our app to save email addresses, we are going to display only the addresses that meet our criterion. Sounds good, yeah? Let's code:

const validEmails = [];

const displayValidEmails = (arr) => {
  arr.forEach(el => {
    if (el.toLowerCase().includes('gmail.com')) {
      validEmails.push(el);
    }
  });
  console.log(validEmails);
};

The checking and displaying actions happen in our displayValidEmails function. So anytime a user uses our app, this function will be invoked with an array of the emails to be checked.

Inside the function, once each email passes the check, we store it in our validEmails variable until all the emails have been checked and stored. Then we display the valid emails to the user.

So, let's assume User A wants to use our app and sends in an array of 2 email addresses: '' and ''. We will call our displayValidEmails function and pass in the provided array:

displayValidEmails(['teni@gmail.com', 'abii@yahoo.com']);

User A would see only '' as that is the only email that satisfies our criterion:

Code output

Let's assume another user, User B, immediately uses our app after User A is done and sends in 3 email addresses: '', '', and ''. Our function is invoked again, but this time around with the 3 new email addresses:

displayValidEmails(['abideen@gmail.com', 'romeo@outlook.com', 'tonya@gmail.com']);

And User B sees this:

Code output

Uh-oh! Yup, uh-oh is right. User B is supposed to see only the 2 email addresses that satisfy the criterion: '' and ''. But he's seeing 3 instead, and the first one is User A's own valid email address.

Users being able to see each other's data was so not what we intended when we designed our app, but that's what's currently happening. This is magic. This is terrible.

But why's our app acting wonky? Well, simply because we made the validEmails variable a global one.

Global variables are very helpful because you can declare them one place and use them anywhere, even inside functions that have no idea when these variables were created. But they can also prove very dangerous because when your function ends, the data stored in the global variables won't be automatically cleared to make way for new data.

How do we end this sorcery? Simple. We change the scope of the variable. By moving validEmails into the displayValidEmails function, we ensure the validEmails is locally scoped to the displayValidEmails function, and as such is always created afresh anytime the function is called.

const displayValidEmails = (arr) => {
  const validEmails = [];
  arr.forEach(el => {
    if (el.toLowerCase().includes('gmail.com')) {
      validEmails.push(el);
    }
  });
  console.log(validEmails);
};

Now, if we re-run User A and User B's actions, User B will see only his own data.

Code output

If we want a more compact code, we can also replace the forEach and push methods with a single filter operation. By so doing, validEmails is automatically made a local variable that provides a clean storage container each time the function is called.

const displayValidEmails = (arr) => {
  const validEmails = arr.filter(
    el => el.toLowerCase().includes('gmail.com')
  );
  console.log(validEmails);
};

Cast the Wrong Spell, Blame the Code

In summary, while humans might be fickle, codes are not. Errors are never random nor is your code doing magic. Codes always behave the way they're designed to behave.

If you make a variable global by failing to declare it inside the function you want to use it in and then expect it to act like a local variable, well, you're setting yourself up for some serious disappointment. So if your code is not working the way you expect, you're either using concepts the wrong way or even writing the wrong syntax.

It's All in the Way You Use it, Baby!

"Does this mean global variables are bad?" Of course not! You will definitely encounter situations that require the use of a global variable.

In our email-check app above, if we decide to give each valid email address a unique serial ID before saving it into the display array, we would have to create a global variable whose count would always increase by 1 whenever a new email is confirmed valid.

By making the ID counter global, we are ensuring that the count won't start afresh whenever the function is called. And that is a good use case of a global variable.

I'm going to conclude by paraphrasing a quote a friend of mine loves to make: If your code's not working, be glad and rejoice because that's your chance to learn something new....or in my case, truly understand a concept you thought you knew.

On that note, I'm glad my bulk-upload operation didn't work as smoothly as I expected, because it helped me solidify my understanding of variables and scoping.

Did you enjoy reading this article? Show some love by leaving a like or a comment, and even more importantly, spreading the knowledge by sharing it with friends. If you've ever been in a similar situation where a simple programming concept nearly messed up your app, do feel free to share in the comments section.

Thanks for reading!!!