Mastering the functions' return statement
To end a function, JavaScript uses the statement return
. The return
statement is very simple. It's one of the first thing you'll learn in a coding course.
But you shouldn't underestimate its capacity to help you write a cleaner and more performing code (and the opposite when you don't master it well enough).
I am using JavaScript code to illustrate my point in this post, but it also applies to other programming languages.
Summary
How does the return
statement work?
We use functions to group together statement which go contextually together. It means they serve the same purpose, do the same task.
First rule we're going to try and follow : one function = one responsibility. Never more than one responsibility. It will make your code so much better.
function addOneTo(a) {
const result = a + 1
return result
}
The function addOneTo(a)
adds 1 to the value contained in the variable a
. I explain JavaScript variables in details in a different post, if you're not comfortable with them yet, I highly encourage you to read it.
The return statement allows us to return the result of the addition this way : return result
.
In other words, the return
statement lets us clearly indicate to the JavaScript interpreter which value this function will return. In our example, we use two variables. We don't want to return a
, only result
.
What about functions with no return statement?
Those of you who, like me, had algorithms classes might remember this : a function must always return something. It's a ground rule. JavaScript follows this rule, functions always return something. In the case where you don't want to return a value, something else exists. It's called procedures.
The problem is: There are no procedures in JavaScript.
To solve this problem, JavaScript allows us to return an “undefined” value. It's not defined. We don't know what the value is, so we can say it's empty.
function prepareObject(obj) {
obj.connect({
// ...
})
obj.use({
// ...
})
return undefined
}
This way, you can use functions as you would use procedures, you put together reusable statements that don't return defined results/values.
The function prepareObject
runs its statements, and at the end we say that the function return an undefined
value. For us, it means that we return nothing, but the interpreter is happy because you return a value anyway.
If you try running the prepareObject
function, you'll see that it returns the value undefined
.
There are a few ways to make the code simpler, though. First, leave the value blank like this :
function prepareObject(obj) {
obj.connect({
// ...
})
obj.use({
// ...
})
return
}
Because “nothing” is returned, the JavaScript interpreter understands that you want to return an undefined
value and does it automatically.
You can also omit the statement altogether, like this :
function prepareObject(obj) {
obj.connect({
// ...
})
obj.use({
// ...
})
}
If you're thinking that is a procedure, I understand why, but it's wrong. Try it, run the prepareObject
function, you'll see that it returns the value undefined
.
The return statement is a “blocking” statement
The fact that return
is a classical statement allows us to put it anywhere in the code of a function.
function doComplicatedStuff(obj) {
obj.connect({
// ...
})
return obj
obj.use({
// ...
})
}
The previous code is valid. The only thing you have to remember here is that return
is a “blocking” statement in the scope of the parent function.
Which means that it indicates the stopping of the function in which it is. It's important to understand because it means that the code is located after a return
statement will never be executed.
The code located after a return
statement is what we call “dead code”.
In fact, the return
statement literally means : “this function is done running properly, here is the result you can show to the parent context. No need to read further.”.
A better use of the return
statement
If you have asked yourself why we would use return;
instead of omitting the statement, or if you've wondered why we would write code after a return
, this part of the post is for you.
Once you understand these two ideas, it becomes possible for you to better your use of the return
statement. A good use of return
makes a for a better code readability.
Getting out of the function as soon as possible
When we read code, whether it's ours or someone else's, the more data, the harder it is for our brains to understand it. Jumps between execution contexts are part of the data that needs to be considered, and each function is a new execution context.
This being said, we don't want to use less functions because there are more pros than cons (re-usability, code structuration, etc…).
A way to reduce the complexity of the code is to reduce the number of data to consider. The return
statement can help us do that. Let's see an example.
function byOrder(a, b) {
let result = null
if (a.order > b.order) {
result = 1
}
if (a.order < b.order) {
result = -1
}
if (a.order == b.order) {
result = 0
}
return result
}
This function is used to tell the JavaScript function array.sort
how to sort an array. It's used like this : array.sort(byOrder)
.
Let's read the function to understand. Let's see what our brain does when we're debugging to illustrate our point. a.order
equals 10
and b.order
equals 0
.
- First, I see that there is a
result
variable initialized with the valuenull
, but the value is going to change because we use thelet
statement. To read more about this step, read the detailed post on JavaScript variables. - One condition : if the order of
a
is superior tob
. It is, in our case, we modify the value ofresult
to1
. - A new condition : if the order of
a
is less thanb
. It's not our case, no need to go into the details of this condition. - A new condition : if the order of
a
is equal tob
. It's not our case here, no need to go into the details of this condition. - We see that the
return
statement wants to return the value of theresult
variable. The function returns1
because we are in the scope of the first condition.
We had to read (and understand) more code than necessary to make sure we understood what this function does. The points 3 and 4 above aren't necessary, but we still have to execute them in our brain. We also had to remember what happened before these steps to know what to return.
Now let's try to rewrite the code of byOrder
so it can be out as soon as possible, thanks to the proprieties of return
. Then we'll do the same exercise to compare the code complexity.
function byOrder(a, b) {
if (a.order > b.order) {
return 1
}
if (a.order < b.order) {
return -1
}
return 0
}
This new function does the exact same thing. It has the same signature as the first function. Let's try to read it now (as a reminder, a.order
equals 10
and b.order
equals 0
) :
- First, I enter the function and there is a condition straight away : if the order of
a
is superior tob
. It is, in our case, so we return the value 1.
This is how the return
statement is blocking, and it stops executing the function, in our specific case, the rest of the code is “dead”. There's no point in reading it. We don't need to understand the rest of the function to understand what it does in our case.
There are a lot less parameters to take into account for our brain during the reading. It's way less complex. It makes me think about theguard
with Swift.
And it's true in almost every case. Try to do the exercise for a.order === b.order
, even if it's the last tested conditions, the second function is less complex.
Make the else
statement more useful
Another way to better the code of byOrder
is to use the keywords else
and else...if
.
function byOrder(a, b) {
let result = null
if (a.order > b.order) {
result = 1
} else if (a.order < b.order) {
result = -1
} else {
result = 0
}
return result
}
We're mimicking the behavior of the solution with the return
because the next conditions aren't executed as soon as a corresponding condition is found.
However, the solution isn't right because I think it's still overloaded, and it requires more focus than the return solution we mentioned above.
Some bypass this problem by using return
. But they forget the point of usingelse
.
function byOrder(a, b) {
if (a.order > b.order) {
return 1
} else if (a.order < b.order) {
return -1
} else {
return 0
}
}
In this specific case, you have to choose : either else
, or return
. The use of return
cancels the pros to use else
and it complicates the code too. This code has no other purpose than to show that the person who wrote didn't really understand the subtleties of else
statement and return
statement. Don't do that.
By using solely the return
in this function, you delete a few words. But more importantly, you delete a full scope (in the last else
) and you delete a level of indentation, which, as we saw before, help to make your code clearer and less complex.
Personally, I rarely use else
anymore in my code because I can, in most cases, write a function that is easier to read and understand by using return
exclusively.
Don't make me say what I didn't say, else
and else...if
statements are very useful structures, I still use them as needed. My point here is that they're only useful in some situation. There's no use using them if there are other keywords or structures that are best fitted for our situation. This advice works for everything in programming.
In short, the return statement (return
) allows returning a value, defined or not, to the parent context of the function. When the return statement is run, it stops the execution of the current function.
It helps to optimize our code writing to reduce the number of scopes and different levels of indentation inside a single function. It also helps prevent having too many data to go through when we're debugging.