Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 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,65 @@
+++
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`.
Comment thread
abdishakoor-dev marked this conversation as resolved.

Choose a return value the caller can't mistake for a real result. Array indices are never negative, so `-1` can only mean "not found", and the caller can check with `if (index === -1)`. A string method that searches for a match has a similar choice to make: returning `''` treats "no match" as an empty result of the same type as a real one, while returning `null` marks it as a different case the caller must check for separately. Either can be the right choice for a given function, but make the choice deliberately, and stay consistent with what similar functions in the same codebase already do.

### 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
Comment thread
abdishakoor-dev marked this conversation as resolved.
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, this is usually the more useful choice for a summary: the loop can skip the bad file, tell the user about it, and still summarise the other 99. But it depends on the situation. If a single bad file means the whole summary can't be trusted, crashing loudly is the better choice instead. | This is the right call when a partial summary would be misleading or unsafe to use. For a plain summary like this one though, recovering and reporting the problem is usually more useful than throwing away 99 files of good work. | This swallows the error: the summary is quietly missing a file and nobody knows to fix it. Whether you continue or crash, don't do it silently."
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 would quietly pass a strange value along instead of surfacing the problem where it happened."
correct="0"
>}}
141 changes: 141 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,141 @@
+++
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");
Comment thread
abdishakoor-dev marked this conversation as resolved.
}
// 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`.

{{<note type="tip" title="What makes a good error message?">}}
A good error message says what went wrong, and ideally, what the actual bad value was. Compare:

```
Error: expected a positive number
```

```
Error: expected a positive number, but got -5
```

The second message saves the reader a trip into the debugger. They can see exactly what was passed in, not just what should have been passed in instead.
{{</note>}}

### 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