Tight coupling, lack of testing, these are probably the most common culprits. Basically the issue is just shoddy standards and procedure.
Yet if you work in a very large codebase, I would add one common one which can occur even with looser coupling and greater care to testing procedure.
The Side Effects of Removing Parasites
Codebases evolve against their current state. If there is something malfunctioning that flies under the radar of testing, and it is allowed to stay that way for years, the tendency of the system will be to evolve against that malfunction, turning it into a symbiotic parasite. To remove the parasite would cause further malfunctions all over the place.
As an analogy (but don't take it too literally, as it's not necessarily caused by a confusion of definitions and semantics), consider that someone mistook an apple for a pear. They started referring to pears as "apples". Hand me those "apples" (person responds by grabbing a basket full of pears). The whole community evolved to understand that an "apple" is actually a "pear" and vice versa learning by example, and started to thrive on this malfunction.
Now, a foreigner comes into the community. "Hands me those apples" -- the foreigner fetches a basket full of what are actually apples. "I said hand me apples, not pears." --"These are apples. Those are pears," says the foreigner. "What???" Now the foreigner grabs an encyclopedia, shows the community their malfunction.
In this case, initially the bug appears to be on the foreigner's side. From the community's standpoint, he's the one making the mistake, and his behavior of fetching a basket full of apples when requested to fetch a basket full of apples, in spite of being the actual correct thing to do, is now perceived as a glitch. When the foreigner corrects the community (fixing the root of the problem), years worth of misbehavior now become exposed as the actual glitch.
That's usually how it goes. This is an analogy, it's not necessarily something that can be fixed by renaming an identifier or updating documentation. But this symptom of a bug fix exposing all kinds of new bugs can occur in such circumstances, where the bug fix really addressed the heart of the issue, but in doing so, unraveled all the glitchy behavior of the codebase that grew, for years, to depend on what was wrong to function correctly. If you simply remove a parasite, there can be all kinds of unanticipated side effects.
This is a human phenomena mistaking coincidence for truth. It's built into all of us yielding superstitions. A mystical crystal makes a person diagnosed with a disease recover as a 1 in a billion case. Now mystical crystals are the correct course of action for someone with a deadly illness, surely the person with the disease could not have been misdiagnosed, doctors are perfect. If a person attempts to correct this glitch, they will unravel an entire history of misbehavior, and it will possibly even turn into a debate of who is right and who is wrong.
Programming can be like that against large teams with a long legacy. To correct a root of a problem can challenge all similar code ever written, and unravel a parasite.
Getting Lucky with
memcpy Against the Linux Kernel
As a totally concrete example, consider an example from Linus Torvalds where someone in user space was using
memcpy in various places for overlapping copies when it's not supposed to do that properly, and even documented as not capable of doing this with a citation to use
memmove in those cases. Yet it luckily worked over many years improperly using
memcpy until an update to the Linux kernel and C runtime, which was perfectly correct (and still preserving the documented behavior of
memmove) made that code which just "luckily worked" over many years run out of luck. In that case, fixes and updates exposed previous bugs that were always there, but just getting lucky because they happened to rely on something that was incorrect but luckily didn't cause any malfunction at the time.
Yet it was bound to happen that lady luck would stop smiling on such misbehavior at some point.
This can't always be protected against with testing since if the community wrote the tests, they would hold up a pear to the system, ask if it's an apple, and the system would respond that it's an apple. It's only possible to verify correctness if you know what "correct" is, so the same mistake that caused the mistake in the first place will leak into the test, checking for incorrect behavior with the assumption that it's the correct behavior.
memcpy misuse case, the people who wrote the code obviously thought it was correct to use
memcpy for overlapping cases. The mistake in even understanding what "correct" means here lead to years of accumulated bugs which silently flew under the radar, until a change was made which revealed all those mistakes made in the past.