Pair it with naming your complex and chained expressions and suddenly you have some seriously readable code.
So far, I have never seen a valid scenario where a switch statement is actually any better than if.
In typed languages a lot of those checks get implemented in your actual types, but you still might have various business logic/data integrity checks you might implement in early return.
Seen in this light, this pattern's really not so much in tension with the idea of having a single return variable. It's just a way to implement the idea that invalid states should not be possible, which you accomplish in your type system when and if possible, and fall back to runtime checks for the gaps where it's not.
That said, I much prefer early return whenever it makes sense. In functions that do have a lot to unwind and many possible points of failure I'll pull out the old villain 'goto' and have the unwind code at the bottom of the function.
Strictly sticking to only one approach is usually a mistake. One that is repeated a lot in computer science. There are schools of thought that if everything is the same it will be easier to understand, but what happens is problems that don't exactly fit the mold end up being solved in awkward and inefficient ways. Or development gets slowed because you have to refactor your problem around the tools instead of the other way around.
This is where Lisp's unwind-protect, finally blocks in some languages, defer in Go, and C++'s RAII pattern can come in handy. Especially if you have a healthy respect for goto and want to minimize its presence in your code for various reasons.
`defer`, on the other hand, is probably the only thing I wanted to take from Go to other languages I worked with. Beautifully explicit and wonderfully useful.
Old:
using (var file = OpenFile())
{
// use file
}
New: using var file = OpenFile();
// file will be disposed when it goes out of scopeIt would be cool if someone made a way to scope variables at the function level in JavaScript instead of at the block level. I might write a transpiler for it…
https://github.com/tc39/ecma262/blob/master/CONTRIBUTING.md
So, impostor syndrome aside, why not? Wanna team up on that?
It would be nice if there were an equivalent in the native API. You can use .finally() with chained promises, but as far as I know there's nothing comparable with async/await yet, and that's a much more comfortable syntax with which to work with promises overall.
function foo(x) {
if (x == null) {
return null;
}
x += 2;
return x;
}
I can't just look at "x += 2;" and know whether or not it's conditionalized. I have to have the full context including the early-return, and then reason about the control flow from there. Whereas: function foo(x) {
if (x == null) {
return null;
} else {
x += 2;
return x;
}
}
Here I can tell just from the else-block that this is one possibility which will execute if the other one does not, and vice-versa. Their indentation is the same, cementing their relationship. If I want to know whether this block will execute I need only look at the if()'s, not their contents.That is, I don't think it matters too much whether you do:
function div(x,y) {
if (y == null) {
return null;
}
if (y == 0) {
return null;
}
return x/y;
}
or function div(x,y) {
if (y == null) {
return null;
} else if (y == 0) {
return null;
} else {
return x/y;
}
}
as they both accomplish the same major benefit of the pattern.The real thing it's helping to avoid is accidentally creating something like this:
function div(x,y) {
if (y != null) {
if (y != 0) {
return x/y;
} else {
return null;
}
} else {
return null;
}
}
which can quickly get very hard to reason about without in more complex cases.In practice, in C-like languages, I tend to see the "else-less" version which is why I brought it up
return nil if n.nil?
I asked on the go-nuts mailing list about perhaps including them in go2, but it seems many of the people who replied hate them.
If people tend to write:
function foo(x) {
if (x == null) {
return null;
}
// do some stuff to the code
for(a reason) {
// do some more stuff
if(some detailed reason) {
return null;
}
// more things
return 7;
}
Then the shape of the code and the "pattern" of early returns gets broken.If you read code where all of the short-circuit, early-return logic is at the start of the function, and then all of the work is done, do you still have this opinion?
E.g.
function foo(x) {
if(x == 1) { return null; }
if(x == 2) { return null; }
if(x > 7) { return null; }
// do things with x
return 7;
}
? (cond
(early-return-condition early-value)
(second-early-condition early-value2)
...
(t final-else-value))
The "default case" is on the same footing as the other cases. Only in procedural languages does the control-flow get confusing if you aren't careful.Also - at the very least - you can mimick the "flat" early-return style and just stick some else's in there and call it a day. That's my main point; less so the actual deeper nesting
function check(x) {
if (!test1 (x)) return false;
if (!test2 (x)) return false;
if (!test3 (x)) return false;
if (!test4 (x)) return false;
return true;
}
In _this_ scenario, it is true that this is much more readable than: function check(x) {
if (!test1 (x)) {
return false;
} else if (!test2 (x)) {
return false;
} else if (!test3 (x)) {
return false;
} else if (!test4 (x)) {
return false;
} else {
return true;
}
}Your first example would still look clean like this:
function check(x) {
if (!test1 (x)) { return false; }
if (!test2 (x)) { return false; }
if (!test3 (x)) { return false; }
if (!test4 (x)) { return false; }
return true;
}
And it would be much safer from stupid copy/paste errors. function check(x) {
if (!test1 (x)) return false;
if (!test2 (x)) return false;
if (!test3 (x)) return false;
if (!test4 (x)) return false;
return true;
}
why wouldn't you do something along the lines of (every (lambda (f) (funcall f x)) (list #'test1 #'test2 #'test3 #'test4a))
or whatever is the equivalent in your preferred language? if (user == null) return false;
if (IsCompleted(user)) return true;
if (action == null) return false;
if (value < 0 || value > 100) return false;
You're not usually just passing a single value to a bunch of "test" functions. function check(x) {
if (!test1 (x)) return false;
else if (!test2 (x)) return false;
else if (!test3 (x)) return false;
else if (!test4 (x)) return false;
else return true;
}
I think this is more clear than either of the aboveTrue. I still think the first is clearer though. Either all tests pass, or they don't.
function check(x) {
return test1(x) && test2(x) && test3(x) && test4(x);
}
Or, since I assume is JavaScript, just: const check = x => test1(x) && test2(x) && test3(x) && test4(x);I put all my early returns at the top as these are the preconditions for the rest of the code; I don't think that's hard to understand.
const foo = x => x !== null ? x + 2 : null
I much preferred (and still greatly prefer in industry) the pattern of exiting the function as soon as possible.
Returning and throwing quickly and strict.
int foo(thing_t \*work) {
int ret = E_SUCCESS;
if(work->thing == BAD_THING) {
ret = E_BAD;
goto exit;
}
do_some(work);
if(work->thing2 == OTHER_BAD_THING) {
LOG("whoopsies");
ret = E_OTHER;
goto exit;
}
ret = do_more(work);
if (!ret) {
LOG("lazy");
goto exit;
}
ret = last_bit_of(work);
exit:
return ret;
}Not sure it makes much sense to indent everything within "if" and if you forget to "else", you've just potentially hidden a bug.
- Return early
- Return often
- Reduce levels of nesting
https://erock.io/2019/07/11/three-rules-for-refactoring-func...
I still see far to many snippets with multiple levels of indentation, where each if branch is for the happy path, and if they were converted to early returns you could flatten the whole thing to 1 indentation level, at the expense of requiring negated boolean expressions which aren't as readable.
_ = !isDefined(user) && throw new Error("User must be defined.");
_ = !isString(user.firstName) && throw new Error("User's first name must be a string");
But I while it is concise, I can also understand why people prefer regular if statements.Doesn't seem to be valid JS, can't remember where I got it from
_ = isDefined(user) || throw new Error("user must be defined")
This reads way more natural for me. "A user is defined OR throw an error"...I've also seen this in Perl (`do_something() || die()`) and shell scripts (`grep -q || die "not found"`).
[0] https://babeljs.io/docs/en/babel-plugin-proposal-throw-expre...
assert(isDefined(user),”User must be defined”) // throws if Falsy
{isLoading && <Loading />}There’s not even a line difference to using if statements correctly (as others in the comments have demonstrated) The one thing an if statement does is checking if something is ‚true‘. I don’t understand why anyone would use a ‚switch‘ here apart from showing how clever they are.
> the script will run from the case where the criterion is met and will run the cases after that regardless if a criterion was met.
Using the switch method won't allow you to return several errors while the simple ifs method described earlier could accumulate the errors and return later. The switch method is elegant though.
Which would look like this: