diff --git a/common-content/en/module/js2/throw-or-return/index.md b/common-content/en/module/js2/throw-or-return/index.md new file mode 100644 index 000000000..0a44ee613 --- /dev/null +++ b/common-content/en/module/js2/throw-or-return/index.md @@ -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`. + +### 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? + +{{}} + +{{}} + +{{}} + +{{}} diff --git a/common-content/en/module/js2/throwing-errors/index.md b/common-content/en/module/js2/throwing-errors/index.md new file mode 100644 index 000000000..0d774e884 --- /dev/null +++ b/common-content/en/module/js2/throwing-errors/index.md @@ -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 {{}}The caller of a function is the code that calls it, not a person. If `main` calls `calculateMedian(list)`, then `main` is the caller.{{}} 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 {{}}Failing fast means stopping as soon as we know something is wrong, instead of carrying on with bad data and failing later somewhere confusing.{{}}. + +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 {{}}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.{{}} 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"); + } + // 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. diff --git a/common-content/en/module/js2/try-catch/index.md b/common-content/en/module/js2/try-catch/index.md new file mode 100644 index 000000000..33b4ad952 --- /dev/null +++ b/common-content/en/module/js2/try-catch/index.md @@ -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 {{}}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.{{}} 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 {{}}Catching an error and doing nothing with it. The problem still happened, but now there is no crash and no message, so nobody knows.{{}}, 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. diff --git a/org-cyf/content/itp/data-groups/sprints/1/prep/index.md b/org-cyf/content/itp/data-groups/sprints/1/prep/index.md index d84294877..cbd1bc4c6 100644 --- a/org-cyf/content/itp/data-groups/sprints/1/prep/index.md +++ b/org-cyf/content/itp/data-groups/sprints/1/prep/index.md @@ -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]]