This site lists the protips that we shared with students during our courses
return
and throw
- two ways to return data from functionsFunctions (or in OOP-languages methods) are the fundamental units where our code is written. They return values using the return
keyword, but sometimes things goes wrong and then we want to be able to indicate this problem to the caller of the functions. Hence functions can return in two ways:
return
throw
-ing an exception, indicating that something bad has happened.Let’s look closer at each, starting with the happy path.
By the way, the content of this blog post is applicable to most C-style languages (C#, Java and JavaScript for example), if I don’t indicate otherwise.
return
for when things goes rightThe following function, written in JavaScript:
const divide = (a, b) => {
return a / b;
}
This simple function returns the quotient (oh yeah, baby! Math nerd acheivment unlocked) of the two sent in values. If everything goes well we expect to get the number returned. Let’s try it
const quotient = divide(4, 2);
console.log(`The quotient is ${quotient}`);
// Prints "The quotient is 2"
Ok - we could actually make the return implicit using arrow functions, by writing it like this:
const divide = (a, b) => a / b;
Which makes for shorter and sweet code. But this is making the return statement implicit since there’s only one line. It really says:
const divide = (a, b) => return a / b;
Ok, all of that is old news. But what happens, in JavaScript, if you do this:
const divide = (a, b) => {
const result = a / b;
}
In fact, that will still return something… undefined
to be precise. It is like JavaScript is inserting the following code, if you don’t have a return
:
const divide = (a, b) => {
const result = a / b;
return undefined;
}
This is not the case in C# and Java, though.
So from this we understand that a function ALWAYS returns a value, even if you have forgotten the return
-statement.
Let’s now check this throw
thing.
throw
for when things goes wrongLet’s be nice and protect the people calling our function from dividing with zero. This has a tendency to be a bad thing…
const divide = (a, b) => {
if(b === 0) {
// eee?! What should I do now?
// It's not return, since it's not ok
}
return a / b;
}
We have now got data that we cannot use. We want to indicate this to the caller of this function. Hence we don’t want to indicate that everything is dandy. So we cannot use return
which is the normal case.
This is what throw
is for. We throw
an exception (in JavaScript called an Error) to indicate something different that return.
A proper implementation of the above would be something like this.
const divide = (a, b) => {
if(b === 0) {
throw new Error("You cannot divide with zero, dummy");
}
return a / b;
}
This is cool, now our function can indicate that something is not as it should, if you passed in 0 as the divisor (I can do this nerd thing all day), by throw
-ing an exception (or error).
And if we can operate on the data we can return
a result and indicate that everything is ok.
But how should we handle that in the calling code
try/catch
This is what the keywords try
and catch
is for. Also known as structured error handling.
Let’s write it first and then talk about the code:
try {
const quotient = divide(4, 2);
console.log(`The quotient is ${quotient}`);
const quotient2 = divide(5, 0);
} catch (error) {
console.log(error.message)
}
Ok - I have wrapped a few calls to divide
in a try
block. If the code inside that block throw
s an exception we will end up in the catch
-block.
In my catch
-block I print the error message to the console
.
That makes it easy for my code to see the difference of a normal, good result and an exceptional bad error result.
So an error is thrown
up in the air and that means that it will be caught in the next catch
-block. So imagine the following structure:
const level1 = () => {
try {
level2()
} catch (error) {
console.log(error.message)
}
console.log("Well, that was the level1 function doing it's thing\nHope you liked it")
}
const level2 = () => {
level3()
}
const level3 = () => {
divide(5, 0)
}
level1()
level1
is called
level1
calls level2
level2
calls level3
level3
calls divide(5, 0)
divide
throws an exception, since we’re about to divide with 0level3
doesn’t have a catch
block so the error continues to bubble uplevel2
doesn’t have a catch
block so the error continues to bubble uplevel1
have a catch
block so the error continues to bubble up
catch
prints the error and then continue with the rest of the lines in it’s function before returning undefined, since no return is writtenIn this way we can see how a throw
n exception keeps travelling upwards until a catch
-block catches it.
If no catch
-block will catch the exception, it will be finally caught by the runtime (Node for example) that handles the error by exiting the program and print the error message to the console.
A common thing that I often see is the following structure:
try {
const quotient2 = divide(5, 0);
} catch (error) {
throw error;
// or throw new Error(error.message);
// or throw new Error("An error has occurred");
}
This is known as re-throwing an exception and is a bit unnecessary, to be honest. We are not adding any new information (Ah well, the last example above, we are replacing the error information, but still).
In fact, if you are just rethrowing the error you would be better off not catching the error at all, and let the callers higher up in the calling chain handle the error.
Like this:
const quotient2 = divide(5, 0);
Yes - if you don’t add any new information or can handle and recover from the error sent-in, you should not catch
it.
What should you do if someone is trying to divide with 0? You cannot change the values and return another quotient… The only sane thing to do is to inform the caller that you cannot operate on this parameters and that something bad would have happened. Then let the caller handle the problem.
If you find yourself rethrowing exceptions, consider if you’re actually adding more information or if you instead should let the caller handle the problem.
Let’s finish up this blog post, by going back to the beginning. There are two ways to return data from a function: return
to indicate success, throw
to indicate failure.
Consider this function:
const unreach = () => {
return "Reached";
throw new Error("Never reached");
}
It’s easy to see that we will never reach that second line, since return
will take us out of the function.
But this is the same situation:
const unreach = () => {
throw new Error("Reached");
return "Never reached";
}
The second line will never be executed, since we have already left the function, through the throw
There are two ways to return data out of a function:
return
returns data and indicate successful completion of the functionthrow
indicates failure and returns some information about the exception that happened.When we return
we can store the result in a variable const result = divide(4, 2);
.
In order to get hold of the information about the exception that happens we need to catch
the error.