Skip to content
63 changes: 63 additions & 0 deletions common-content/en/module/js2/throw-or-return/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
+++
title = 'Throw or return?'

time = 10
[objectives]
1='Decide whether a function should throw an error or return a value'
2='Identify where in a program an error should be handled'
[build]
render = 'never'
list = 'local'
publishResources = false

+++

We now have two ways for a function to respond when it meets a problem: return a value, or throw an error. How do we choose?

### Expected situations: use a return value

If a situation is a normal part of using the function, handle it with ordinary code. Searching an array for a value that isn't there is not an error; that's why [`indexOf`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) returns `-1` rather than throwing. The caller expects both outcomes and can check with an `if`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add some guidance on what value to choose, maybe with some examples? e.g. that -1 is a good thing to return from indexOf because it probably fits in with how it's used (whereas returning null would be more annoying), that string methods may want to return "" or null depending on context, etc?


### Broken expectations: throw

If the function _cannot do what its name promises_, throw. `calculateMedian("apple")` can't calculate any median; returning a made-up value would hide a bug in the caller's code. Throwing makes the problem loud and points at where it was detected.

A useful question to ask: **is this situation something the caller should have prevented?** Passing a string to `calculateMedian` is a bug in the calling code, so throw. A user typing their name into a date field is not a bug, it's expected human behaviour, so handle it with normal conditional logic.

### Where should errors be caught?

_Throwing_ tells us the function can't do its job. _Catching_ is about something different: who can do something useful with that information?

Only catch an error where you can do something useful with it: skip one bad record and continue, use a default value instead, or show the user a helpful message. If the code you're in can't do any of those things, don't catch the error there. Let it travel up to a caller who can. If nobody can do anything useful with the error, it's better for the program to crash with a clear error trace, because that shows exactly what needs to be fixed.

### 🧰 Decide for yourself

For each scenario, decide what the code should do before you check the answer. Some of these are judgement calls, so what matters most is your reasoning: is this situation expected or a broken expectation, and who can do something useful about it?

{{<multiple-choice
question="The function formatAs12HourClock is called with '25:00'. There is no such time. What should formatAs12HourClock do?"
answers="Throw an error | Return '25:00 am' | Return undefined"
feedback="Right. The function cannot format a time that does not exist: the caller should have prevented this, so failing fast points at the real bug. | This looks like an answer, so the caller carries on displaying a nonsense time to users. | Returning undefined is quieter than throwing but just as unhelpful: the caller gets a strange value with no explanation of what went wrong."
correct="0"
>}}

{{<multiple-choice
question="A user types 'eleven' into a form field asking how many tickets they want. What should the program do?"
answers="Check the input and show the user a helpful message | Throw an error | Nothing, the input is fine"
feedback="Right. Users typing unexpected things is normal human behaviour, not a bug, so handle it with ordinary conditional logic. | A person mistyping is an expected situation, not a broken expectation in our code. Throwing turns normal use into a crash. | 'eleven' is not a number, so the program cannot book eleven tickets without dealing with it somehow."
correct="0"
>}}

{{<multiple-choice

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd maybe answer this with "it depends"? Sometimes "continue despite bad data" is a useful answer for a user and sometimes "error if anything is wrong" is a useful answer...

question="A program producing a summary of 100 data files finds that one file is corrupted. What should the summary loop do?"
answers="Catch the error, report the bad file, and continue | Let the whole program crash | Silently skip the file"
feedback="Right. The loop can do something useful with the error: skip the bad file, tell the user about it, and still summarise the other 99. | Crashing throws away 99 files of good work when the loop could recover and report the problem instead. | This swallows the error: the summary is quietly missing a file and nobody knows to fix it."
correct="0"
>}}

{{<multiple-choice
question="divide(a, b) is called with b equal to 0. What should divide do?"
answers="Throw an error | Return 0 | Return Infinity because that is what JavaScript does"
feedback="Right. divide cannot do what its name promises with a zero divisor, and a zero here usually means a bug in the calling code. Some teams make a different, documented choice, but it must be a deliberate one. | Zero is a made-up answer that hides the caller's bug and will be used as if it were real. | JavaScript's own operator does evaluate to Infinity, but for our function that quietly passes a strange value along instead of surfacing the problem where it happened."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
feedback="Right. divide cannot do what its name promises with a zero divisor, and a zero here usually means a bug in the calling code. Some teams make a different, documented choice, but it must be a deliberate one. | Zero is a made-up answer that hides the caller's bug and will be used as if it were real. | JavaScript's own operator does evaluate to Infinity, but for our function that quietly passes a strange value along instead of surfacing the problem where it happened."
feedback="Right. divide cannot do what its name promises with a zero divisor, and a zero here usually means a bug in the calling code. Some teams make a different, documented choice, but it must be a deliberate one. | Zero is a made-up answer that hides the caller's bug and will be used as if it were real. | JavaScript's own operator does evaluate to Infinity, but for our function that would quietly pass a strange value along instead of surfacing the problem where it happened."

correct="0"
>}}
127 changes: 127 additions & 0 deletions common-content/en/module/js2/throwing-errors/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
+++
title = 'Throwing errors'

time = 20
[objectives]
1='Explain what happens when an error is thrown'
2='Throw an error when a function cannot do its job'
[build]
render = 'never'
list = 'local'
publishResources = false

+++

By now, `calculateMedian` should handle lists of both odd and even length. Here's one way to do that, which we'll build on for the rest of this page:

```js
function calculateMedian(list) {
const middleIndex = Math.floor(list.length / 2);
if (list.length % 2 === 0) {
// Even length: average the two middle values
return (list[middleIndex - 1] + list[middleIndex]) / 2;
}
// Odd length: there's a single middle value
return list[middleIndex];
}
```

But what should it do with input like this?

```js
calculateMedian([]);
calculateMedian("banana");
calculateMedian("apple");
calculateMedian(["ten", "twenty", "thirty"]);
```

There is no median of an empty list. A string isn't a list of numbers at all. And a list of strings isn't a list of numbers either, even though it _is_ a list. But our implementation doesn't know any of that. It will happily return `NaN` for the empty array, `NaN` again for `"banana"`, a plausible-looking `"p"` for `"apple"`, and `"twenty"` for the list of strings, a value that looks entirely plausible, so nobody would even think to question it. The results aren't just wrong, they're unpredictable: the same kind of bad input produces a different kind of bad output depending on exactly what you pass in.

This is a problem. The {{<tooltip title="caller">}}The caller of a function is the code that calls it, not a person. If `main` calls `calculateMedian(list)`, then `main` is the caller.{{</tooltip>}} of `calculateMedian` gets back a value that _looks_ like an answer, and carries on using it. The program doesn't fail here, where the mistake happened. It fails later, somewhere else, when that nonsense value gets used. Bugs like this are hard to track down because the error trace points far away from the real cause.

When a function cannot do its job, it shouldn't guess. It should fail immediately and loudly, at the point where the problem is. We call this {{<tooltip title="failing fast">}}Failing fast means stopping as soon as we know something is wrong, instead of carrying on with bad data and failing later somewhere confusing.{{</tooltip>}}.

What about calling it with no argument at all?

```js
calculateMedian();
```

This time our implementation does crash, straight away:

```console
TypeError: Cannot read properties of undefined (reading 'length')
```

Here, at least, we get an error message. But it comes from the JavaScript engine, not from us, and it doesn't explain what's wrong: it doesn't mention `calculateMedian`, or say that a list was expected. Whoever hits this has to read our code to work out what actually went wrong. If we check for a missing argument ourselves, we can throw a message that says exactly that, straight away.

### Using `throw`

In Structuring Data, you interpreted error traces when JavaScript threw a `SyntaxError` at you. We can also {{<tooltip title="throw">}}Throwing an error stops normal execution immediately. The error travels up through the calling functions until something handles it. If nothing handles it, the program crashes and prints the error trace.{{</tooltip>}} errors from our own code, using the `throw` keyword:

```js
function calculateMedian(list) {
// Checking for a non-array (including no argument at all)
if (!Array.isArray(list)) {
throw new Error("calculateMedian requires an array of numbers");
}
// Checking for an empty array
if (list.length === 0) {
throw new Error("calculateMedian requires a non-empty array");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't check they're all numbers - do we want to?

}
// Checking for non-number items in the array
for (const item of list) {
if (typeof item !== "number") {
throw new Error("calculateMedian requires an array of numbers");
}
}
const middleIndex = Math.floor(list.length / 2);
if (list.length % 2 === 0) {
return (list[middleIndex - 1] + list[middleIndex]) / 2;
}
return list[middleIndex];
}
```

[`Array.isArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) checks whether a value is an array. We also check every element is a number, using `typeof`. If the input isn't an array, is empty, or contains anything that isn't a number, we `throw` a new [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) with a message explaining what went wrong.

Save this in a file and try calling `calculateMedian([])`. The program stops and prints an error trace, just like the ones you interpreted before, except this time the message is one we wrote ourselves:

```console
Error: calculateMedian requires a non-empty array
at calculateMedian (/Users/cyf/prep/median.js:8:11)
```

Now the error points at the exact place the problem was detected, with a message saying what the problem is. That's much more useful to whoever calls our function than a mysterious `NaN`.

### Testing that a function throws

We can assert that a function throws using the [`toThrow`](https://jestjs.io/docs/expect#tothrowerror) matcher:

```js
test("throws an error when given an empty list", () => {
expect(() => calculateMedian([])).toThrow(
new Error("calculateMedian requires a non-empty array")
);
});

test("throws an error when given something that isn't an array", () => {
expect(() => calculateMedian("banana")).toThrow(
new Error("calculateMedian requires an array of numbers")
);
});

test("throws an error when given an array with a non-number", () => {
expect(() => calculateMedian(["ten", "twenty", "thirty"])).toThrow(
new Error("calculateMedian requires an array of numbers")
);
});

test("throws an error when given no argument", () => {
expect(() => calculateMedian()).toThrow(
new Error("calculateMedian requires an array of numbers")
);
});
```

Note that we wrap the call in a function. When we call a function with arguments, those arguments get evaluated before the function is called. If we called `expect(calculateMedian([])).toThrow()` directly, the error would be thrown before `expect` was called, and so before `expect` could check anything.
65 changes: 65 additions & 0 deletions common-content/en/module/js2/try-catch/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
+++
title = 'try/catch'

time = 15
[objectives]
1='Use a try/catch block to handle a thrown error'
2='Explain why errors should not be silently ignored'
[build]
render = 'never'
list = 'local'
publishResources = false

+++

`calculateMedian` now throws when it can't do its job. By default, a thrown error crashes the program. Sometimes that's exactly right. But sometimes we know what to do when something goes wrong, and we'd rather do that than crash.

Suppose we're producing a report of median salaries for several teams, and one team's data is bad:

```js
const salaryDatasets = [
[42000, 51000, 60000],
[],
[38000, 45000],
];
```

If we call `calculateMedian` on each dataset in a loop, the empty array will throw, and the whole report will crash. The bad data for one team stops us reporting on all the others.

### Catching errors

We can {{<tooltip title="catch">}}Catching an error stops it travelling any further up through the calling functions. Execution continues from the catch block, and the program does not crash.{{</tooltip>}} a thrown error with a `try...catch` block:

```js
for (const salaries of salaryDatasets) {
try {
const median = calculateMedian(salaries);
console.log(`The median salary is ${median}`);
} catch (error) {
console.error(`Skipping a dataset: ${error.message}`);
}
}
```

JavaScript _tries_ to run the code inside the `try` block. If any of it throws, execution jumps immediately to the `catch` block; the remaining statements in the `try` block do not run. The thrown error is passed in as `error`, and we can read the message we wrote with `error.message`.

Run this and you'll see the report is produced for the first and third datasets, with a warning about the empty one in between. One bad dataset no longer stops the whole program:

```console
The median salary is 51000
Skipping a dataset: calculateMedian requires a non-empty array
The median salary is 41500
```

### Don't hide errors

A `catch` block must _do_ something with the error: log it, use a fallback, or tell the user. Look at this version:

```js
try {
const median = calculateMedian(salaries);
console.log(`The median salary is ${median}`);
} catch (error) {}
```

This is worse than not catching at all. The error is silently thrown away, so bad data no longer crashes the program _and_ nobody finds out about it. This is called {{<tooltip title="swallowing an error">}}Catching an error and doing nothing with it. The problem still happened, but now there is no crash and no message, so nobody knows.{{</tooltip>}}, and it turns loud, easy-to-find bugs back into quiet, hard-to-find ones. If you can't do anything useful with an error, don't catch it.
9 changes: 9 additions & 0 deletions org-cyf/content/itp/data-groups/sprints/1/prep/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ src="module/js2/side-effects"
name="Implementing all the cases"
src="module/js2/more-median-cases"
[[blocks]]
name="Throwing errors"
src="module/js2/throwing-errors"
[[blocks]]
name="try/catch"
src="module/js2/try-catch"
[[blocks]]
name="Throw or return?"
src="module/js2/throw-or-return"
[[blocks]]
name="Solving problems with arrays 📼"
src="module/js2/arrays-workshop"
[[blocks]]
Expand Down
Loading