Skip to main content

Special cases broke Microsoft Zune and can ruin your code base too

·15 mins

Read about the pitfalls of special cases in programming, illustrating how they can lead to complexity, diminish readability, and create maintenance challenges.

It’s December 31, 2008, and you’re about to listen to Coldplay’s hit song Viva La Vida on your Microsoft Zune. The device – which had been working smoothly since you got it in 2006 – refuses to turn on.

As it turns out, neither you nor your device is special. Every Zune across the world isn’t working because of a poorly implemented special case buried in the code.

The issue, covered by Adam Barr in his book The Problem with Software and in a range of articles after the incident, came from firmware code meant to handle the device’s calendar. The code, reproduced below, contained a subtle mistake.

year = ORIGINYEAR;
while (days > 365)
{
    if (IsLeapYear(year))
    {
        if (days > 366)
        {
            days -= 366;
            year += 1;
        }
    }
    else
    {
        days -= 365;
        year += 1;
    }
}

The code followed a simple logic: Subtract 365 days and increment the number of years by one. And if it’s a leap year, subtract 366 days. Theoretically, every time this code runs, you should get the correct number value.

A leap year is a textbook example of a special case and the Zune team accounted for leap years – but not well. When the initial value of days is 366, the code passes the special case offered by the while loop. But because the initial year is 1980, making the IsLeapYear predicate true, the code then tests for whether there are more than 366 days. This is false, so the code doesn’t subtract days or add years.

As Brian Hayes wrote at the time, “We do nothing at all, because there’s no else clause attached to this if statement. We simply return to the head of the while loop, where we find that days is still greater than 365, and so we go through the same motions again, and again, ad infinitum.”

The day the Zunes died is a dramatic example of a programming pattern that often goes wrong yet remains widespread. The pattern repeats because it almost always feels like a “just one-time thing”: Developers encounter problems that don’t fit their initial design, build in a special case to handle the exception, and hope that everything runs as intended.

Sometimes, this means a worldwide breakage. More often, special cases create, over time, codebases that are hard to read and hard to maintain. Eventually, seemingly tiny choices can accumulate and you can find yourself turning around to look back in surprise and horror at a codebase glued together by special cases.

Why make special cases #

Special cases are moments when programmers encounter business requirements or design problems that emerge in the midst of ongoing work and decide to build new and often single-use code to account for them.

Special cases are inevitable. They are not necessarily mistakes. And yet, when developers decide to implement special cases carelessly, entire codebases can start to suffer. And by then, the solution isn’t as simple as reeling the special cases back in.

If developers could simply avoid special cases, most likely would. But almost every special case has a good reason for existing, even if that reason isn’t good enough to warrant the long-term, codebase-wide costs. Still, any given special case has a rationale and if we want to make fewer special cases, then we need to think through why we keep making them.

Example: External business requirements #

As a hypothetical and simple example, let’s imagine we’re building a website for a newspaper. We designed a function – an illustrative one, not a high mark for Python perfection – to display the five most recent articles on the homepage. The initial version looks like this:

def display_recent_articles(articles):
    sorted_articles = sorted(articles, key=lambda a: a.published_date, reverse=True)
    return sorted_articles[:5]

This function takes a list of articles, sorts them in reverse order by the date of publication, and returns the five most recent items. It’s straightforward and easy to understand. But, as is always the case in the realities of the working world, a stakeholder has added new business requirements. With each wave of requirements, and each – we’re told – needs to be done ASAP, special cases appear necessary and the code becomes increasingly complex.

If the business decides, for example, that they want to cover politics but not shove political content into the faces of every reader, they might ask us to make it so that articles from certain categories won’t display on the homepage. We could then modify the function with a special case:

def display_recent_articles(articles, exclude_categories=None):
    if exclude_categories is None:
        exclude_categories = []
    filtered_articles = [a for a in articles if a.category not in exclude_categories]
    sorted_articles = sorted(filtered_articles, key=lambda a: a.published_date, reverse=True)
    return sorted_articles[:5]

But that might not be the end of it. Some articles aren’t getting as much traffic as others but a stakeholder thinks those articles have the best chance of offering the newspaper some notoriety. So they ask you to modify the function again so that the homepage can display “featured” articles even if they’re not in the five most recent:

def display_recent_articles(articles, exclude_categories=None):
    if exclude_categories is None:
        exclude_categories = []
    featured_articles = [a for a in articles if a.featured and a.category not in exclude_categories]
    non_featured_articles = [a for a in articles if not a.featured and a.category not in exclude_categories]

    sorted_featured = sorted(featured_articles, key=lambda a: a.published_date, reverse=True)
    sorted_non_featured = sorted(non_featured_articles, key=lambda a: a.published_date, reverse=True)

    return (sorted_featured + sorted_non_featured)[:5]

Another wave of business requirements rolls in: The analytics have shown the formerly local newspaper is surprisingly popular outside its initial region. Leadership then asks you to modify the function again, so that the homepage can display different new articles depending on where a given user resides:

def display_recent_articles(articles, region, exclude_categories=None):
    if exclude_categories is None:
        exclude_categories = []
    regional_articles = [a for a in articles if a.region == region and a.category not in exclude_categories]
    other_articles = [a for a in articles if a.region != region and a.category not in exclude_categories]

    sorted_regional = sorted(regional_articles, key=lambda a: a.published_date, reverse=True)
    sorted_other = sorted(other_articles, key=lambda a: a.published_date, reverse=True)

    return (sorted_regional + sorted_other)[:5]

Over time and with each business requirement, the function grew more and more complex. If you had known all of these requirements from the beginning, your initial design might have been entirely different – but how often do you know everything you need to know before beginning?

Example: Internal duct-taping #

New requirements aren’t always handed down from above. Sometimes, we see a problem, envision a solution, and discover problem after problem as we implement it.

Let’s say you’re building the basic logic for a library of CMS components. You need a component that can display a block of text within an image. You might start with the image being over the text but, of course, different images create different limitations.

So, you start creating special cases for different image ratios. Is the image too tall? Put the text to the right of the image instead. Well, if you’re doing that, provide options so the text can be on top or under too.

Already, you can see the beginning of a spiral leading to greater and greater complexity. The code goes downhill fairly quickly and what could have been a straightforward component can end up being much more complicated than you would have expected from looking at the functionality alone.

Instead of realizing there was a design problem, we made as many special cases as necessary and duct-taped them together. Every special case appeared to justify the next but in the end, we have more duct-tape code than good code.

The consequences of special cases #

If you’ve had a stressful day, there’s nothing inherently wrong with ordering delivery for dinner. But if you don’t have a general sense of what qualifies as a day that warrants a special treat nor a sense of how many special treats your budget can handle, then you can end up spending yourself into even more stress.

It’s a fundamental problem of human cognition: It’s always tempting to do a small thing now and always difficult to measure how the accumulation of those small things creates outsized consequences.

The same pattern applies to special cases. Most developers know it’s best to only make special cases on occasion and only as necessary, but without real criteria for making these decisions, it’s easy to lean on instinct that can you lead you astray.

The fictional examples above illustrate the workflow that leads to using special cases, but in the real world, the problem can be exponentially worse when special cases exist throughout the codebase and not just in one example. The consequences that we describe below are similarly worse because more areas of the codebase can be affected.

The solution is as political as it is technical. If the business requirements demanding special cases keep emerging while work is in progress, how can you involve business stakeholders earlier and determine their needs before committing to a design?

The continuous interjection of new requirements can sometimes make the business side feel like opposition, but the goal has to involve figuring out how to work with them as partners rather than defeating or resisting them as enemies.

But even with all that in order, special cases are still likely to happen. How, then, can you build in a process for regular refactoring and periodic rewriting? Without rework, codebases will inevitably become hard to read, test, and maintain.

Hard to read #

Too many special cases creates code that’s difficult to read at a glance. Special cases don’t just make code dense, though, because each exception calls back to and complicates its preceding logic.

With enough special cases, reading code can begin to feel like reading a scammy coupon. At a glance, you can see “40% off” but once you try to actually understand the deal, lines and lines of fine print make it impossible. (Hobby Lobby, the craft store, was actually sued for this in 2016).

Image of a super savings coupon, showing 40% of in Hobby Lobby.
Image of a super savings coupon, showing 40% of in Hobby Lobby.

Hard-to-read code isn’t just an annoyance. Once code can’t be understood at a glance, it becomes much more difficult for you and other developers to notice mistakes or identify errors. As your company grows, it can also be difficult to onboard new developers and teach them how to read the confusing code. Those new developers will take longer to be productive and if the code is convoluted enough, the developer experience might never be good.

And as time goes on, future changes to the code will take longer and each change will be riskier and more expensive. This long-term cost is often one of the most convincing cases for the business side, however, so if new business requirements causing new special cases are a persistent issue, track the effects so that you can communicate better with business-side partners.

Hard to test #

Code connected by webs of special cases is almost invariably harder to test than simpler, more concise code. Special cases thread in so many different scenarios and so much adjacent logic that it can be difficult to figure out coverage and difficult to feel confident that your unit tests actually have the coverage they need to have.

For example, the engineering team behind Textio, a word processor, encountered innumerable bugs as they built a mobile app and an improved developer experience. Writing in Increment, the engineers explain that “each new bug felt like a unique challenge” and that they “were often tempted to write special cases for each one.” Instead, they write, they “made a point of looking for deeper patterns present in several issues, an approach that allowed us to pare back our solution and keep it small. Ultimately, the strategy led to fewer and easier-to-diagnose bugs.”

Hard to maintain #

When the amount of special cases grows to the point of making code hard to read, code also becomes hard to maintain. When there are too many special cases, it’s difficult to zoom out and see the consequences of changes for the system as a whole and difficult to zoom in to see the granular details.

Often, these problems don’t emerge until the development team grows, the company scales, or the codebase gets large. One developer will create a function that works on a basic level but the code will involve an if/else chain with dozens of conditions (and sometimes, that complex chain will be embedded in another and that one in another and…). When other developers try to change, improve, or extend the code, they’re stuck adding yet another condition and hoping the whole application survives.

Code quality depends on warding off bad code #

What is good code? It’s a question with a range of answers and an even wider range of directions. Do we optimize for simplicity? Performance? Maintainability?

Most of these definitions, despite how widely they vary, focus on what good code is and how we can add it. But special cases show us that the best code is as much the reality of avoiding bad code as adding good code.

Special cases get added because they are necessary (at least in the moment) but codebases work best when special cases are avoided and short-term progress is sacrificed for long-term quality and maintainability.

Beware of organic growth #

Huge changes and gargantuan PRs are the bane of your reviewers, so it’s tempting to flip to the other extreme and only contribute minimal changes.

Over time, though – just like in special cases – minimal changes can accumulate and create technical debt in their wake. Focusing exclusively on tiny changes is like running while wearing blinders. By the time you finish and take the blinders off, you might not be where you’re supposed to be and if you are, you might find out you’ve taken a pretty inefficient route.

Instead, refactor regularly to maintain a standard of cleanliness. When you find special cases, name and encapsulate them to turn them into normal cases or recast the problem so that special cases vanish.

Web developer Christopher Bojarski puts the former method well, explaining how, by naming and encapsulating, “we reduce the risk of introducing unstable conditional code. We can deal with all cases at their level of abstraction while not cluttering up the calling code. We get all the tools of object-oriented design to improve those special cases because they are isolated.”

And Jonah Goldstein, writing for Cyberark, puts the latter method well, explaining why recasting code is more effective than enumeration and testing it. “If you’ve missed any, or if any of your tests are incorrect or incomplete, your code will fail,” he writes. But if you recast the problem, you can make special cases vanish. “There are many fewer ways to fail with the latter strategy,” he writes. “And much less code to maintain.”

Beware of new functions and features #

Some of the worst code is the result of heedless addition. New functions and features, whether dreamed up by you or proposed by a teammate or stakeholder, will always provide some exciting novelty. It will always be tempting to think first of what the new function or feature can do and think little about whether that really needs doing.

Every time a new feature or function is proposed, be wary of building them. Remember: “Every line of code written comes at a price: maintenance.” Every addition should come with demonstrated confidence that the new feature or function will be used and that the value provided will be greater than the complexity you’re adding to the codebase.

Special cases emerge again here because they provide a way to add and add and add. Instead, focus on building and maintaining a light feature set that works across situations.

Here, it’s important to communicate carefully but clearly with the business side. They often have good reasons for wanting new features but frequently lack visibility into how long it might take to build those features and the ongoing maintenance cost of those features. It’s better to be transparent and treat prioritization as a shared problem so that you can work together to focus on the features and changes that are the most important. That way, you can reduce extraneous feature requests and minimize sudden calls for special cases.

Beware of scope creep #

Discussions of code quality inevitably bring up discussions of rewriting and refactoring code so as to get rid of the code we’ve discovered is bad. It’s a worthy discussion that tends to fall prey to binary thinking.

Don’t slip to either extreme and waste your time rewriting all of your code or avoid wasting time so ruthlessly that you never revisit your code. The former extreme creates perfectionism, resulting in code that’s functional but does too little. The latter extreme creates carelessness, resulting in code that’s extensive and brittle or functional and unreadable.

Instead, consider other ways of incorporating quality checks into your process. Full rewrites might be too intense to do often, but scrubbing your code regularly might be more practical. Maintaining zero tech debt might be impossible but if you regularly do light refactors, you might be able to ensure your codebase is always as clean as you left it.

There are process-level tools too, such as allocating a fixed amount of time to refactoring in each sprint measured by the number of hours or story points. The goal matters more than the method – figure out how to align development needs with business goals and reinforce the habit of refactoring to beat back the addition of special cases over time.

Code quality dies by a thousand cuts #

Code quality feels abstract until it’s not. When you’re debating the fine details – recall tabs vs. spaces in Silicon Valley or naming variables Id or ID – the conversation can be both passionate and empty. It’s like writers debating AP style and the Oxford comma; people care but people know, ultimately, that the only thing that matters is whether your article makes sense or your code works.

If you lean too far into this perspective though, you can end up complacent. Code rarely becomes “bad” due to a single mistake. Code quality suffers slowly and then all at once.

Special cases are especially dangerous because they’re not mistakes. There are many occasions when it’s good and right to use them. But without a sense of when to look for other solutions or a sense of higher-level quality concerns, a heedless approach to making special cases can create a codebase that’s hard to recover from.