1

At some organizations I'm seeing a trend where if a bug is reported, engineers will directly rewrite the offending code without knowing the root cause. In general this will fix the bug, but in my opinion it also introduces the risk of new bugs - and it doesn't allow a team to learn collectively from the mistake that caused the bug in the first place.

In general, when a bug is reported, I try to retrace the steps that the programmer took to find the root cause - or the root 'mistake' that caused it - but I'm aware that at some point in time this research efforts might accumulate to more work than if we would have rewritten is, especially if the code is particularly old.

What are some other considerations to take into account when making this decision? Here are the ones I currently have, as a list:

Method 1: Rewrite the code immediately

(+) Makes the code up to 'modern' standards that the team has set

(-) Introduces risk for new, unknown behavior and bugs

Method 2: Find the root cause and fix it

(+) Typically requires less code changes, and therefor less risk and code to review

(-) Old/outdated code doesn't get updated and may still have some errors

3
  • 1
    A huge factor in the degree of change you're willing to introduce is whether or not you have a large body of tests. Often an old code base doesn't so, you're going to break somethings; the question is how much and when you find out. When that's the case, sadly a minimal patch makes a lot of sense.
    – Erik Eidt
    Commented Dec 11, 2022 at 15:59
  • I don't think real-world bugfixing can be simplified sensibly into "rewrite the code or fix the root cause". There are way-too-many different kind of bugs and root causes (and, it is not even clear in your question whether by "root cause" you mean the root cause in the code, or the human factor's root cause).
    – Doc Brown
    Commented Dec 12, 2022 at 10:43
  • Rewriting code to "fix" a bug without understanding the cause of the bug is (almost) never a good idea. If it's some sort of timing bug caused by bad use of threads and timers that gets random problems then rewriting it may be the correct approach. I had exactly that scenario at my last job. But in general, the dev should understand the bug, not blindly rewrite the code.
    – DaveG
    Commented Feb 7 at 22:41

6 Answers 6

5

Perhaps you are using "root cause" in a different way but identifying and fixing the root cause doesn't fix the bug. The root cause(s) are the underlying factors that led to the defect being injected in the first place. They are almost always process issues, where a process was not followed or an appropriate process was not in place.

The problem seems to be more about figuring out how big to scope the work. When fixing a bug, I would consider the smallest amount of work to be creating at least one test case that fails because of the existence of the bug and then the code changes needed to make all the added test cases pass. If you have good test coverage around the other system functionality, then you can be confident that you haven't introduced other bugs while fixing the first.

In cases where the assumption doesn't hold and you don't have good (preferably automated) test coverage around what needs to change to act as regression tests, you need to make the decision about how many tests to create to give that confidence and how to make sure you're not spending too long on getting the fix out. That decision is highly context-sensitive and needs to be made by the team. You may want to look at characterization tests and other ideas from Michael Feathers' Working Effectively with Legacy Code.

When standards change and existing code does not evolve to maintain those standards, the team is also incurring technical debt. Failure to pay down the technical debt will slow down development. Depending on your team and organizational structure, I've found it helpful to keep track of discrete units of work that can be taken on to pay down technical debt in your issue tracker. If you can associate that work with the impacted parts of the system and quantify the costs or risks of not doing that work, the team can make informed decisions about the ongoing maintenance of the codebase, when to do it based on when certain subsystems are changing, and the costs of not doing it. Keeping technical debt low will make it easier to make changes faster.

5
  • Just adding a few tests which fail and then changing the minimum of code necessary to make the tests pass is IMHO not enough - this approach can too easily be "gamed". Instead, the test case(s) should be representative for the kind of input data which can cause the bug, and the code changes should be aiming to fix this whole category of bugs which are represented by those cases. ....
    – Doc Brown
    Commented Dec 12, 2022 at 10:58
  • ... For example, lets say my new square root function sqrt has numerical issues and it returns sqrt(9)=3.0001, though I expect to to be accurate by at least 10 digits for any number below 10000. I can add some test cases for sqrt(9), sqrt(400) and maybe sqrt(10000), and make the tests pass by hardcoding the results for exactly these numbers into the function. Or, I could replace the numerical algorithm for the square root calculation (which might be more effort to implement, but the right thing to make sqrt not brittle).
    – Doc Brown
    Commented Dec 12, 2022 at 11:04
  • 1
    @DocBrown That's a fair point, and what I was thinking of. I wouldn't expect a professional to game a test and defects like that.
    – Thomas Owens
    Commented Dec 12, 2022 at 11:55
  • 1
    Of course, I made this extreme example to make my point clear. But what I have seen in reality is that professional devs approached a bug which manifested itself in some unexpected exception by catching the exception and swallow it (which requires very little effort), whilst the right to do would have been to identify why the exception was thrown in the first place (that is what I would call "looking for the root cause"). Unfortunately, the latter can be much harder, since one has to dig through several layers and needs to understand a larger part of the system.
    – Doc Brown
    Commented Dec 12, 2022 at 12:32
  • 1
    @DocBrown I wouldn't call that the root cause. I would keep digging. Why was the exception thrown without proper handling? I'd try to get into the design decisions, requirements, actual versus intended use of the system to figure out the real root cause. To me, the real root cause goes beyond the code and into why the code is what it is.
    – Thomas Owens
    Commented Dec 12, 2022 at 12:53
3

In addition to @ThomasOwen's answer, which I agree with, I miss something more when it comes to considering (or not) rewriting code and it's context.

We, developers, more often than not have very little perspective of the whole. Intended or not, we barely know the overall picture and it's especially true when we speak about the planning.

When I find myself in a similar situation, my first impulse is always to fix the problem, no matter what it takes. I'm a problem solver. To some extent, because I'm supposed to be something more than a braindead lemming chopping and digging.

After many years of rushing at doing the first, I learnt that I have to be aware of the project's stage. Sometimes the decision to fix a bug that requires a code refactor is not on me but on the PM.

PMs hold accountability for many decisions, sometimes even if a bug should be fixed or not and the way to do it. We have to know when we can do something or when we should ask first. Here, we call that "raising the hand".

Most of the time, during the coding phase, we won't need the OK to keep going. It's expected to find and solve bugs as they appear. But 1 week before the release... Sorry, but no. We are not allowed to make that decision unilaterally if we can't measure accurately the impact on the planification. Even if we have a large plan of automated tests. What it's expected from us then is to reach the PM and explain the situation, give solutions (as PM I wouldn't buy only 1) and the pros and cons of each of them, so the PM (and the rest of the stakeholders) can make the best decision possible.

So, besides testing, be aware of the project's stage. Understand the issue and its cause so you can size the solution accordingly and, if the solution is not that simple, then do nothing without the due approval. You should not reduce things to a simple method 1 or 2 and nothing else. It's tempting to oversimplify things, but going with methods 1, 2 or 3 is contextual too.

1

The normal, optimal, case should be to repair the code (despite often being unsatisfactory). (However see below.) Especially if the business logic and its implementation is large, and not entirely clear.

If the bug is not covered by a unit test, or there are no unit tests, it becomes time to provide them. So you will see regression errors. Especially if you consider a rewrite that is a good investment.

If the code is ugly, or you see a technical debt, you should tackle improving the code too. That again adds a bit satisfaction. Run a code style checker like SonarLint.

A rewrite can promise more elegant code, better future maintenance (readable, extendible). It should be done in parallel, so you may run old and new version simultaneously. It can also be done when there are reasons to change the user interface.

Internationalisation was realized by importing Excel sheets, selecting two columns for two languages (source and target). By changing the dialog to first pick the Excel sheet, you can give a preview of some 7 rows, pick the columns and languages, and check whether there is a header line. Better traceability!

-1

If you have an ancient code base, in dire need of refactoring or even rewriting, but no budget for it, then a bug in the code will be used as an excuse to make more changes.

There are situations where code is so bad that nobody dares touching it. So in practice you can’t fix a bug because you have no idea how other parts of your code are affected by your changes. So rewriting it is very tempting. On the other hand, that horrible complicated code might be horrible and complicated for some good reason. That’s the risk you take.

Regarding the comments: I am not a Boy Scout. There are situations where I know what should be done and can’t do it and I’m happy to get an excuse. Like you might ask a customer to complain about something so your boss tells you to do what you wanted to do anyway. Being able to spend more time on an UI that the boss doesn’t care about because the UI team tells you to improve it. (Boss doesn’t give me time to improve X. I tell UI team X isn’t right. They tell my boss. He tells me to fix it.)

New code can change/break things elsewhere: That’s exactly the problem when you have code that is so bad it is impossible to find out what it does, and even harder to find out what it is supposed to do. I’m quite sure I mentioned that.

2
  • 1
    You can call this "an excuse". Or you can call it "the boyscout principle" (leave a code base in a better state behind than you found it). Improving the code around some bug can be more effort than just fixing the bug. But when it comes to evaluating where it pays off most to refactor some code, the places where bugs were found are often excellent candidates.
    – Doc Brown
    Commented Dec 12, 2022 at 11:06
  • ... and rewriting a certain piece of code (instead of making a local fix) can affect other parts of the code base (or the behaviour) even more, so this argument is IMHO not conclusive.
    – Doc Brown
    Commented Dec 12, 2022 at 12:21
-1
  1. Create a test case that shows the bug
  2. Make it work
  3. Make it beautiful
  4. Make shure the test still passes

Also known as Red, Green, Refactor in test driven development. A test case doesn't have to bee automated, but it's certainly more convenient and keeps you from introducing the same bug again.

-2

You'll have to rewrite the code if the "Root Cause" code happens to be outside your control, in a different domain/part of a binary, etc.

When you're trying to find the root cause of a bug, you might have a better chance if the root cause is within the codebase you're writing in - which would mean you can rewrite the root cause and fix the core issue.

However, the more frameworks and libraries you use, the higher the likelihood the root cause of the bug is an updated library, or a specific combination of frameworks colliding, and then you...kind of can't fix the root cause short of removing frameworks, libraries, possibly entire features until you can work around that cause. In my first job in a co-op, we had used a back-end framework that had an idiom of using a specific annotation structure rather than manually doing a thing; however, it was designed to interact with a simpler variant of the UI framework we used, and the newer framework wouldn't work with it.

In a situation like the one above, it's easier to just rewrite the code that shows the specific bug, and have the root cause analysis as a separate task. If it's in your code, you can update it and undo the code rewrite when it works better without it - if it doesn't, you can note when the change was required, and come back to that section when you have the time and budget to tackle the actual root cause, and track down possible side effects of making a larger change like that.

5
  • 1
    This doesn't answer the question -- the OP is asking about the actual 'root cause' of why the bug exists, and how that buggy code managed to reach production undetected. - i.e. the human factor (for example - badly worded requirements, misunderstood existing code, poor testing/lack of automated tests, programmer mistake, etc. See Thomas' answer above. Commented Dec 12, 2022 at 7:46
  • @BenCottrell: Part of the human factor would involve checking that a different approach to the code could, in fact, actually fix the bug via a quick rewrite and get production moving along - at that point, if it does in fact work, the diff of the change helps as a way to identify the root cause (i.e. Was that line changed recently [And thus introduced by another human request], or has the bug not been in production since the change, and thus something else was involved. Providing that history in the repo helps with further root cause analysis. Commented Dec 12, 2022 at 7:57
  • Except 'root cause' isn't code, it's the chain of reasoning which led to the code being written. (e.g. decisions and assumptions, and not necessarily by a programmer since it could include stakeholder decisions to follow a flawed process or introduce a flawed requirement that conflicts with existing requirements), so 'fixing' a root cause isn't changing code, it's correcting whatever path led to a programmer writing that code in the first place, firstly to identify whether there are any other symptoms and also to avoid new symptoms being introduced later. Commented Dec 12, 2022 at 8:10
  • @BenCottrell: Fixing the code isolates where to start looking for the stakeholder decision/flawed process or flawed requirement. It's much harder to find out the root cause if you don't know the proximate cause; rewriting the code gives you a proximate cause beyond "This stopped working.". Commented Dec 12, 2022 at 8:29
  • 1
    Root cause for a bug might be “boss wanted this code finished an hour before I left for holidays and wouldn’t accept no as an answer”. I had one with a root cause “developer thought he was so smart that he could make changes without code review, unlike everyone else in the company”.
    – gnasher729
    Commented Dec 13, 2022 at 8:24

Not the answer you're looking for? Browse other questions tagged or ask your own question.