Six Factors That Raise The Risk Of Bugs In A Codebase
Tags: bugs, architecture, patterns, codequality
Published: 2024-03-04 06:58:42

As a frontend developer, I've come across several key factors that greatly impact the presence of bugs in a codebase. While my insights are based on JavaScript applications, these principles are relevant across different codebases. Let's explore them together.

**1. Lack of Static Code Analysis**
Static code analysis tools like [TypeScript](https://www.typescriptlang.org/) and [ESLint](https://eslint.org/) play a crucial role in identifying and preventing bugs. TypeScript provides static typing, enhancing the robustness of the code. ESLint detects issues and enforces coding standards. The absence of these tools can significantly elevate the likelihood of bugs due to the lack of early detection and guidance provided during development.

```javascript
// Example of TypeScript usage
// Define a function with static typing
function greet(name: string): string {
  return 'Hello, ' + name;
}
```

**2. Lack of Automated Testing**
Codebases devoid of automated tests, such as unit and end-to-end tests, are at risk of too many bugs. Without automated tests, the detection of regressions and bugs relies heavily on manual testing, leaving room for oversight and undetected issues. Consequently, developers may hesitate to introduce changes or refactor existing code, fearing unintended consequences. When working on a codebase that lacks automated tests, it becomes really hard to ship new features with confidence.

```javascript
// sample unit test with jest
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

// sample end to end test with playwright
test.describe("porfolio page", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/portfolio");
  });

 test("can view the tech jobs project", async ({ page }) => {
    await page.getByTestId("techjobs").click();
    await expect(page).toHaveURL("http://localhost:3000/portfolio/techjobs");
});
)
```

**3. Lack of a Continuous Integration Build Process**
Codebases lacking a continuous integration (CI) build process miss out on automated checks for production readiness, code quality, and test coverage. Without CI, discrepancies between developer environments and the production environment may lead to undetected bugs surfacing in production. Depending on where your code repository is located, you can setup a CI workflow using [GitHub Actions](https://docs.github.com/en/actions), [Bitbucket Pipelines](https://bitbucket.org/product/features/pipelines), [Gitlab CI](https://docs.gitlab.com/ee/ci/) etc.

Example of a GitHub Action workflow that checks the formatting of files, builds for production, checks linting and runs test cases

```yaml
name: CI

on:
  push:
    branches:
      - develop
      - master

  pull_request:
    branches:
      - develop
      - master

jobs:
  actions:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: cache dependencies
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: yarn

      - name: install dependencies
        run: yarn

      - name: check formatting of files
        run: yarn format:check

      - name: build for production
        run: yarn build

      - name: check linting of files
        run: yarn lint

      - name: run unit tests
        run: yarn test:unit

```


**4. Misleading Variable or Function Names**
A variable or function is considered misleading if its name does not accurately reflect its functionality. This discrepancy can lead to bugs when developers rely on the function's name to guide their implementation logic. For instance, consider the function `isLoggedIn`. One would expect it to return true if a user is logged in and false otherwise. However, imagine if the function returns a user object when a user is logged in and null otherwise. While a user object is a truthy value and null is a falsy value, such discrepancies in return types can result in unintended bugs.

```javascript
// Better
const isLoggedIn = () => auth.currentUser ? true : false;
OR
const isLoggedIn = () => !!auth.currentUser;

// Misleading
const isLoggedIn = () => auth.currentUser ? auth.currentUser : null;
```

**5. Multiple Sources of Truth for the Same State**
Having multiple sources of truth for the same state can lead to bugs that are hard to trace or debug. It introduces inconsistency and confusion into the codebase, making it difficult to maintain and debug issues. Consider the example below

```javascript
// Vue.js

<template>
 <input type="text" v-model="loanBalance">
</template>

<script setup lang="ts">
const loanBalance = ref<number>(0)

const getLoanBalance = async (userId: string) => {
 const response = await axios.get(`/loans/users/${userId}`)
 loanBalance.value = response.data.data;
}

const updateLoanBalance = (balance: number) => loanBalance.value = balance
</script>
```
The component above has three sources of truth for the state `loanBalance`: the input field (two-way data binding), the `getLoanBalance` function, and the `updateLoanBalance` function. When a state is modified in many places, such multiple side effects can lead to bugs that are hard to trace and debug.

**6. Redundant States**
A component contains redundant states if the same value is stored in more than one state. This becomes problematic if you have to _manually_ keep track of the states and ensure they contain the same value.

```javascript
// Vue.js

interface Notification {
 subscribers: string[];
 message: string
}

 const notification = ref<Notification>(
 { 
   subscribers: [],
   message: ''
 }
)

const subscribers = ref<string[]>([])

const getNotification = async () => {
 const response = await axios.get('/notifications')
 notification.value = response.data.data;
 subscribers.value = notification.value.subscribers
}
```
In the example above, the list of subscribers is contained in both the notification state and the subscribers state. Having to manually ensure that both states contain the same value can lead to bugs.

However, if subscribers need to be accessed across multiple places in the component, it is much better to use a computed property to ensure that you don't have to manually synchronize the values.

```javascript
 // Vue.js
const notification = ref<Notification>(
 { 
   subscribers: [],
   message: ''
 }
)

const subscribers = computed(() => notification.subscribers))

// React
const [notification, setNotification] = useState({})

const subscribers = useMemo(() => {
  return notification.subscribers
 }, [notification.subscribers]
)

```

In conclusion, the pursuit of bug-free code is an ongoing endeavor. Throughout this article, we've explored six key factors that contribute to the proliferation of bugs within a codebase. From the absence of static code analysis and automated testing infrastructure to the presence of misleading variable names and duplicate states, each factor presents its own set of challenges and risks. By acknowledging these factors and implementing strategies to mitigate their impact, developers can strive towards building more robust and resilient software applications.