protips

Logo

This site lists the protips that we shared with students during our courses

View the Project on GitHub appliedtechnology/protips

return and throw - two ways to return data from functions

Functions (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:

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 right

The 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 wrong

Let’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 throws 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.

Throw?! Throw up, more likely

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()

In this way we can see how a thrown 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.

Re-throwing exceptions

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.

Getting out of the function

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

Summary

There are two ways to return data out of a function:

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.