简体   繁体   中英

How do you write the error checking part of a method to make it readable and error prone?

I had a disagreement with another programmer on how to write a method with a lot of error checking:

public void performAction() {
  if (test1) {
    if (test2) {
      if (test3) {
        // DO STUFF
      } else {
        return "error 3";
      }
    } else {
      return "error 2";
    }
  } else {
    return "error 1";
  }
}

-

public void performAction() {     
  if (!test1) {
    return "error 1";
  }
  if (!test2) {
    return "error 1";
  }
  if (!test3) {
    return "error 1";
  }
  // DO STUFF
}

To me, the deep nesting of if statements makes the first example hard to read.
The second one, despite having three return s, is more readable.

I checked by curiosity what Code Complete was saying about that and it left me less sure about the way to handle this:

The stack of error conditions at the bottom of the nest is a sign of well-written error-processing code.

but then

Indenting the main body of the routine inside four if statements is aesthetically ugly, especially if there's much code inside the innermost if statement.

and considering using guard clauses, as in the second example

Minimize the number of returns in each routine. It's harder to understand a routine when, reading it at the bottom, you're unaware of the possibility that it returned some-where above.

How do you write the error checking part of a method to make it readable and error-prone?

Nothing gets programmers into a fight faster than stylistic debates ( Is it ever advantageous to use 'goto' in a language that supports loops and functions? If so, why? ). So the short answer is "Whatever style you and your team decide is best for your project/language."

That being said, I would like to add my 2 cents to Code Complete's comment about multiple returns. You should make a distinction between multiple successful returns, and multiple returns. If I need to investigate 5 returns that are not due to errors being generated, the function probably needs to be rewritten. If you gracefully exit your function immediately after an error is detected, then a maintenance programmer (ie you in 6 months) should have no more trouble following the main logic of your function than if you had nested all of those error checks.

So, my personal opinion is that your second code example is the better choice.

This is my opinion.

The old mantra of "Minimize the number of returns in each routine" seem to be a bit dated. It is highly applicable when you have methods longer 8-10 lines of code, where lots of operations are executed.

The newer schools of thought, emphasizing Single Responsibility and very short methods, would seem to render that a bit unnecessary. When your whole method does not do any operations directly, but simply handles the error processing, multiple returns in a clean format would be best.

In either case, any time you have nested ifs, the readable suffers substantially.

The one optimization I would make is to use an if-else-if structure, to clearly indicate the logic flow.

Sample code:

public void Execute() 
{     
    if (test1)
    {
        return;
    }
    else if (test2)
    {
        return;
    }
    PerformAction();
}

private void PerformAction()
{
    //DO STUFF
}

If you're using a language with exception-handling and automated resource management, your colleagues should probably get used to your preferred style with premature exits in the case of encountering a external input error.

The idea of trying to shift function exits towards the bottom of the scope was useful in the days before exception handling and automated resource management (ex: languages without destructors or GC like C) because error recovery often required manual cleanup .

In those manual cleanup cases, it was often useful to shift the exits towards the bottom of a function so that you could look at the top of the function for the logic creating the temporary resources needed by the function and towards the bottom of the function to see the symmetrical clean up of those resources.

In such cases as with assembly, it's quite common to see jumps/branches to an error label at the bottom of the function where the clean up would occur. It's also not too uncommon even in C using gotos for this purpose.

Also, as mentioned, deep nesting introduces a lot of mental overhead. Your brain has to function like a deeply-nested stack trying to remember where you're at, and as even Linus Torvalds, a diehard C coder, likes to say: if you need something like 4 nested levels of indentation in a single function, your code is already broken and should be refactored (I'm not sure I agree with him about the precise number, but he has a point in terms of how it obfuscates logic).

When you move into a more modern language like C++, you now have automated resource management via destructors. Functions should then no longer be mentioning cleanup details, as the resources should handle the cleanup automatically by conforming to what's called the resource acquisition is initialization idiom (not exactly the best name). That eliminates one of the big reasons to favor a style that strives to have error handling logic towards the bottom.

On top of that, when you use a language like C++, it potentially throws exceptions and all over the place. So it's not uncommon for every other line of code to have the effect of having a hidden, implicit exit with logic like this:

if an exception occurs:
    automatically cleanup resources and propagate the error

So there are hidden, premature exits all over the place. So if you use a language like that, not only should you get used to premature exits in the case of an exception, but you're kind of forced into it and have no other choice. As far as readability/traceability is concerned in those languages, you can't get any simpler than:

if something bad happened:
     return error

The one exception I'd suggest to the rule is static branch prediction. If you're writing very performance-critical code where the smallest of micro-efficiencies counts more than readability, then you want your branches to be weighted towards favoring the common case line of execution as Intel advises. So instead of:

if something exceptional happened:
    return error

... for performance you might invert the logic and do this instead:

if something normal happened:
     ...
     return success
return error

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM