Add error handling fundamentals to Data Groups Sprint 1#1949
Add error handling fundamentals to Data Groups Sprint 1#1949abdishakoor-dev wants to merge 8 commits into
Conversation
✅ Deploy Preview for cyf-common ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for cyf-curriculum ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
e21ffc4 to
8feddc2
Compare
Trainees currently learn fetch, promises, and async/await without being taught how to throw or catch an error. Adds three js2 blocks (throwing errors, try/catch, throw vs return) built around the existing calculateMedian exercise, and wires them into Sprint 1 prep. Refs #1946
8feddc2 to
70093be
Compare
illicitonion
left a comment
There was a problem hiding this comment.
LGTM! Left a few minor comments.
Exercise-wise, as well as updating the existing ones, do we want to add a few new exercises to get people throwing and catching more?
Also, do we want to add a callback to this in https://curriculum.codeyourfuture.io/sdc/tools/sprints/5/prep/ to reflect on static checking being better than dynamic checking?
|
|
||
| ### Using `throw` | ||
|
|
||
| In Structuring Data, you [interpreted error traces](/itp/structuring-data/sprints/2/prep/#interpreting-this-error) when JavaScript threw a `SyntaxError` at you. Now we can {{<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: |
There was a problem hiding this comment.
This link works, but could plausibly break quite easily (e.g. if that block was re-titled) and we would be unlikely to notice (the link would just start going to the first prep block). Accordingly, we generally try to avoid linking directly to specific prep blocks.
I don't love that. Maybe we could at some point add a "check if the anchors linked to actually exist" check so we'll notice if they break and can fix them.
I'm not sure I'm suggesting a specific action here, but just an FYI...
|
|
||
| ### Using `throw` | ||
|
|
||
| In Structuring Data, you [interpreted error traces](/itp/structuring-data/sprints/2/prep/#interpreting-this-error) when JavaScript threw a `SyntaxError` at you. Now we can {{<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: |
There was a problem hiding this comment.
| In Structuring Data, you [interpreted error traces](/itp/structuring-data/sprints/2/prep/#interpreting-this-error) when JavaScript threw a `SyntaxError` at you. Now we can {{<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: | |
| In Structuring Data, you [interpreted error traces](/itp/structuring-data/sprints/2/prep/#interpreting-this-error) 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) { | ||
| if (Array.isArray(list) === false) { |
There was a problem hiding this comment.
| if (Array.isArray(list) === false) { | |
| if (!Array.isArray(list)) { |
| throw new Error("calculateMedian requires an array of numbers"); | ||
| } | ||
| if (list.length === 0) { | ||
| throw new Error("calculateMedian requires a non-empty array"); |
There was a problem hiding this comment.
We don't check they're all numbers - do we want to?
| }); | ||
| ``` | ||
|
|
||
| Note that we wrap the call in a function. If we called `calculateMedian([])` directly, the error would be thrown before `expect` could check anything. |
There was a problem hiding this comment.
| Note that we wrap the call in a function. If we called `calculateMedian([])` directly, the error would be thrown before `expect` could check anything. | |
| 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. |
|
|
||
| ```js | ||
| test("throws an error when given an empty list", () => { | ||
| expect(() => calculateMedian([])).toThrow(); |
There was a problem hiding this comment.
Shall we add an error message (fragment) to the toThrow() call, to show people a better example of usage?
| } | ||
| ``` | ||
|
|
||
| [`Array.isArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) checks whether a value is an array. If the input isn't an array, or is an empty one, 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. |
There was a problem hiding this comment.
I think it's probably worth an aside talking about what a good error message looks like?
e.g. a common piece of feedback I give in code reviews is "Include an indication of what the bad data was"
|
|
||
| ### 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`. |
There was a problem hiding this comment.
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?
| correct="0" | ||
| >}} | ||
|
|
||
| {{<multiple-choice |
There was a problem hiding this comment.
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...
| {{<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." |
There was a problem hiding this comment.
| 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." |
Matches review feedback on PR #1949.
12ca65f to
961a67b
Compare
Matches review feedback: the guarded implementation checked the input was a non-empty array, but not that its elements were numbers, so calculateMedian(["a", "b"]) would still silently return nonsense.
Calling with no argument does crash on its own (list is undefined), but with a generic engine error that doesn't mention calculateMedian or say a list was expected. The existing non-array guard already catches this case, so add an example and test showing that.
toThrow(string) only checks the message contains a substring, not that it equals it. toThrow(new Error(...)) is Jest's documented way to assert the exact message, more precise given we now write the messages ourselves. Also add the missing non-array test case, and bump time to 20 minutes to reflect the added content.

First attempt for #1946
Adds three js2 blocks (Throwing errors, try/catch, Throw or return?) that teach exceptions and error handling using the existing calculateMedian exercise, and wires them into Data Groups Sprint 1
prep, after "Implementing all the cases".
Trainees currently learn fetch, promises, and async/await without ever being taught how to throw or catch an error. This covers the synchronous foundation.
Two follow ups needed once this lands:
in Non-trivial tweaks to Data Flows sprints 2 and 3 #1111)
Preview: https://deploy-preview-1949--cyf-curriculum.netlify.app/itp/data-groups/sprints/1/prep/#throwing-errors