up
This commit is contained in:
parent
83b93e5992
commit
9ad9063d00
742 changed files with 884 additions and 779 deletions
|
@ -0,0 +1,47 @@
|
|||
The difference becomes obvious when we look at the code inside a function.
|
||||
|
||||
The behavior is different if there's a "jump out" of `try..catch`.
|
||||
|
||||
For instance, when there's a `return` inside `try..catch`. The `finally` clause works in case of *any* exit from `try..catch`, even via the `return` statement: right after `try..catch` is done, but before the calling code gets the control.
|
||||
|
||||
```js run
|
||||
function f() {
|
||||
try {
|
||||
alert('start');
|
||||
*!*
|
||||
return "result";
|
||||
*/!*
|
||||
} catch (e) {
|
||||
/// ...
|
||||
} finally {
|
||||
alert('cleanup!');
|
||||
}
|
||||
}
|
||||
|
||||
f(); // cleanup!
|
||||
```
|
||||
|
||||
...Or when there's a `throw`, like here:
|
||||
|
||||
```js run
|
||||
function f() {
|
||||
try {
|
||||
alert('start');
|
||||
throw new Error("an error");
|
||||
} catch (e) {
|
||||
// ...
|
||||
if("can't handle the error") {
|
||||
*!*
|
||||
throw e;
|
||||
*/!*
|
||||
}
|
||||
|
||||
} finally {
|
||||
alert('cleanup!')
|
||||
}
|
||||
}
|
||||
|
||||
f(); // cleanup!
|
||||
```
|
||||
|
||||
It's `finally` that guarantees the cleanup here. If we just put the code at the end of `f`, it wouldn't run.
|
|
@ -0,0 +1,38 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Finally or just the code?
|
||||
|
||||
Compare the two code fragments.
|
||||
|
||||
1. The first one uses `finally` to execute the code after `try..catch`:
|
||||
|
||||
```js
|
||||
try {
|
||||
work work
|
||||
} catch (e) {
|
||||
handle errors
|
||||
} finally {
|
||||
*!*
|
||||
cleanup the working space
|
||||
*/!*
|
||||
}
|
||||
```
|
||||
2. The second fragment puts the cleaning right after `try..catch`:
|
||||
|
||||
```js
|
||||
try {
|
||||
work work
|
||||
} catch (e) {
|
||||
handle errors
|
||||
}
|
||||
|
||||
*!*
|
||||
cleanup the working space
|
||||
*/!*
|
||||
```
|
||||
|
||||
We definitely need the cleanup after the work has started, doesn't matter if there was an error or not.
|
||||
|
||||
Is there an advantage here in using `finally` or both code fragments are equal? If there is such an advantage, then give an example when it matters.
|
609
1-js/08-error-handling/1-try-catch/article.md
Normal file
609
1-js/08-error-handling/1-try-catch/article.md
Normal file
|
@ -0,0 +1,609 @@
|
|||
# Error handling, "try..catch"
|
||||
|
||||
No matter how great we are at programming, sometimes our scripts have errors. They may occur because of our mistakes, an unexpected user input, an erroneous server response and for a thousand of other reasons.
|
||||
|
||||
Usually, a script "dies" (immediately stops) in case of an error, printing it to console.
|
||||
|
||||
But there's a syntax construct `try..catch` that allows to "catch" errors and, instead of dying, do something more reasonable.
|
||||
|
||||
[cut]
|
||||
|
||||
## The "try..catch" syntax
|
||||
|
||||
The `try..catch` construct has two main blocks: `try`, and then `catch`:
|
||||
|
||||
```js
|
||||
try {
|
||||
|
||||
// code...
|
||||
|
||||
} catch (err) {
|
||||
|
||||
// error handling
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
It works like this:
|
||||
|
||||
1. First, the code in `try {...}` is executed.
|
||||
2. If there were no errors, then `catch(err)` is ignored: the execution reaches the end of `try` and then jumps over `catch`.
|
||||
3. If an error occurs, then `try` execution is stopped, and the control flows to the beginning of `catch(err)`. The `err` variable (can use any name for it) contains an error object with details about what's happened.
|
||||
|
||||
**So, an error inside `try` does not kill the script: we have a chance to handle it in `catch`.**
|
||||
|
||||
Let's see the examples.
|
||||
|
||||
- An errorless example: shows `alert` `(1)` and `(2)`:
|
||||
|
||||
```js run
|
||||
try {
|
||||
|
||||
alert('Start of try runs'); // *!*(1) <--*/!*
|
||||
|
||||
// ...no errors here
|
||||
|
||||
alert('End of try runs'); // *!*(2) <--*/!*
|
||||
|
||||
} catch(e) {
|
||||
|
||||
alert('Catch is ignored, because there are no errors'); // (3)
|
||||
|
||||
}
|
||||
|
||||
alert("...Then the execution continues");
|
||||
```
|
||||
- An example with error: shows `(1)` and `(3)`:
|
||||
|
||||
```js run
|
||||
try {
|
||||
|
||||
alert('Start of try runs'); // *!*(1) <--*/!*
|
||||
|
||||
*!*
|
||||
lalala; // error, variable is not defined!
|
||||
*/!*
|
||||
|
||||
alert('End of try (never reached)'); // (2)
|
||||
|
||||
} catch(e) {
|
||||
|
||||
alert(`Error: ${e.name}`); // *!*(3) <--*/!*
|
||||
|
||||
}
|
||||
|
||||
alert("...Then the execution continues");
|
||||
```
|
||||
|
||||
Please note that if the code structure is violated, like a figure bracket is left unclosed, then `try..catch` can't help. Such errors are fatal, the engine just cannot run the code.
|
||||
|
||||
There is a better term for errors that we are catching: "an exceptional situation" or just "an exception". It's much more precise, meaning exactly the situation when a already-running and well-formed code meets a problem.
|
||||
|
||||
For all built-in errors, the error object inside `catch` block has two main properties:
|
||||
|
||||
`name`
|
||||
: Error name. For an undefined variable that's `"ReferenceError"`.
|
||||
|
||||
`message`
|
||||
: Textual message about error details.
|
||||
|
||||
There are other non-standard properties in most environments. One of most widely used and supported everywhere is:
|
||||
|
||||
`stack`
|
||||
: Current call stack: a string with information about the sequence of nested calls that led to the error. Used for debugging purposes.
|
||||
|
||||
|
||||
````warn header="`try..catch` only works in synchronous code"
|
||||
If an exception happens in the future code, like those inside `setTimeout`, then `try..catch` won't catch it:
|
||||
|
||||
```js run
|
||||
try {
|
||||
setTimeout(function() {
|
||||
noSuchVariable; // script will die here
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
alert( "won't work" );
|
||||
}
|
||||
```
|
||||
|
||||
That's because at the moment of running the function from `setTimeout`, the current script will have already been finished, the engine will have left `try..catch` contruct.
|
||||
|
||||
To catch an exception inside a scheduled function, `try..catch` must be inside that function.
|
||||
````
|
||||
|
||||
````warn header="`try..catch` only works for runtime errors"
|
||||
For `try..catch` to work, the code must be runnable. In other words, it should be valid Javascript.
|
||||
|
||||
It won't work if the code is syntactically wrong, for instance it has unmatched figure brackets:
|
||||
|
||||
```js run
|
||||
try {
|
||||
{{{{{{{{{{{{
|
||||
} catch(e) {
|
||||
alert("The engine can't understand this code, it's invalid");
|
||||
}
|
||||
```
|
||||
|
||||
The Javascript engine first reads the code, and then runs it. The errors that occur on the reading phrase are called "parse-time" errors and are unrecoverable (from inside that code). That's because the engine can't understand the code as a whole, and `try..catch` is a part of it.
|
||||
|
||||
So, `try..catch` can only handle errors that occur in the valid code. Such errors are called "runtime" errors.
|
||||
````
|
||||
|
||||
## Using "try..catch"
|
||||
|
||||
Let's explore a real-life use case of `try..catch`.
|
||||
|
||||
As we already know, JavaScript supports method [JSON.parse(str)](mdn:js/JSON/parse) to read JSON-encoded values.
|
||||
|
||||
Usually it's used to decode the data received over the network, from the server or another source.
|
||||
|
||||
We receive them and call `JSON.parse`, like this:
|
||||
|
||||
```js run
|
||||
let json = '{"name":"John", "age": 30}'; // data from the server
|
||||
|
||||
let user = JSON.parse(json); // reading the object
|
||||
|
||||
// now user is an object with properties from the string
|
||||
alert( user.name ); // John
|
||||
alert( user.age ); // 30
|
||||
```
|
||||
|
||||
More detailed information about JSON you can find in the chapter <info:json>.
|
||||
|
||||
**If `json` is malformed, `JSON.parse` generates an error, so the script "dies".**
|
||||
|
||||
Are we satisfied with that? Of course, not!
|
||||
|
||||
This way if something's wrong with the data, the visitor will never know that (unless he opens developer console).
|
||||
|
||||
And people really really don't like when something "just dies" without any error message.
|
||||
|
||||
Let's use `try..catch` to handle the error:
|
||||
|
||||
```js run
|
||||
let json = "{ bad json }";
|
||||
|
||||
try {
|
||||
|
||||
*!*
|
||||
let user = JSON.parse(json); // <-- error happens
|
||||
*/!*
|
||||
alert( user.name ); // doesn't work
|
||||
|
||||
} catch (e) {
|
||||
*!*
|
||||
// ...the execution jumps here
|
||||
alert( "Our apologies, the data has errors, we'll try to request them one more time." );
|
||||
alert( e.name );
|
||||
alert( e.message );
|
||||
*/!*
|
||||
}
|
||||
```
|
||||
|
||||
Here we use `alert` only to output the message, but we can do much more: do a network request, suggest an alternative way to the visitor, send the information about the error to logging facility... All much better than just dying.
|
||||
|
||||
## Throwing own errors
|
||||
|
||||
Imagine for a minute that `json` is syntactically correct... But doesn't have a required `"name"` property:
|
||||
|
||||
```js run
|
||||
let json = '{ "age": 30 }'; // incomplete data
|
||||
|
||||
try {
|
||||
|
||||
let user = JSON.parse(json); // <-- no errors
|
||||
*!*
|
||||
alert( user.name ); // no name!
|
||||
*/!*
|
||||
|
||||
} catch (e) {
|
||||
alert( "doesn't execute" );
|
||||
}
|
||||
```
|
||||
|
||||
Here `JSON.parse` runs normally, but the absense of `"name"` is actually an error for us.
|
||||
|
||||
To unify error handling, we'll use `throw` operator.
|
||||
|
||||
### "Throw" operator
|
||||
|
||||
The `throw` operator generates an error.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
throw <error object>
|
||||
```
|
||||
|
||||
Technically, we can use anything as an error object. That may be even a primitive, like a number or a string, but it's better to use objects, preferrably with `name` and `message` properties.
|
||||
|
||||
Javascript has many built-in constructors for standard errors: `Error`, `SyntaxError`, `ReferenceError`, `TypeError` and others. We can use them to create objects as well.
|
||||
|
||||
Their syntax is:
|
||||
|
||||
```js
|
||||
let error = new Error(message);
|
||||
```
|
||||
|
||||
For built-in errors (not for any objects, just for errors), the `name` property is exactly the name of the constructor. And `message` is taken from the argument.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let error = new Error("Things happen :k");
|
||||
|
||||
alert(error.name); // Error
|
||||
alert(error.message); // Things happen :k
|
||||
```
|
||||
|
||||
Let's see what kind of error `JSON.parse` generates:
|
||||
|
||||
```js run
|
||||
try {
|
||||
JSON.parse("{ bad json o_O }");
|
||||
} catch(e) {
|
||||
*!*
|
||||
alert(e.name); // SyntaxError
|
||||
*/!*
|
||||
alert(e.message); // Unexpected token o in JSON at position 0
|
||||
}
|
||||
```
|
||||
|
||||
As we can see, that's a `SyntaxError`.
|
||||
|
||||
...And in our case, the absense of `name` can be treated as a syntax error also, assuming that users follow a sort of "schema" that requires the existance of `"name"`.
|
||||
|
||||
So let's throw it:
|
||||
|
||||
```js run
|
||||
let json = '{ "age": 30 }'; // incomplete data
|
||||
|
||||
try {
|
||||
|
||||
let user = JSON.parse(json); // <-- no errors
|
||||
|
||||
if (!user.name) {
|
||||
*!*
|
||||
throw new SyntaxError("Incomplete data: no name"); // (*)
|
||||
*/!*
|
||||
}
|
||||
|
||||
alert( user.name );
|
||||
|
||||
} catch(e) {
|
||||
alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
|
||||
}
|
||||
```
|
||||
|
||||
In the line `(*)` the `throw` operator generates `SyntaxError` with the given `message`, the same way as Javascript would generate itself. The execution of `try` immediately stops and the control flow jumps into `catch`.
|
||||
|
||||
Now `catch` became a single place for all error handling: both for `JSON.parse` and other cases.
|
||||
|
||||
## Rethrowing
|
||||
|
||||
In the example above we implemented error handling for incorrect data. But is it possible that another unexpected error happens in `try {...}` block?
|
||||
|
||||
Of course, it is! Normally, a code is a bag with errors. It's typical that even in an open-source utility like `ssh` that is used by millions for decades -- suddenly a crazy bug is discovered that leads to terrible hacks. Not to mention other similar cases.
|
||||
|
||||
In our case, `catch` block is meant to process "incorrect data" errors. But right now it catches everything.
|
||||
|
||||
For instance, say, we made a programming error, a mistype:
|
||||
|
||||
```js run
|
||||
try {
|
||||
|
||||
// ...
|
||||
JSON.papaparse(); // a mistype, no such function
|
||||
|
||||
} catch(e) {
|
||||
alert( "JSON Error: " + e.message ); // JSON Error: JSON.papaparse is not a function
|
||||
}
|
||||
```
|
||||
|
||||
By nature, `catch` gets all errors from `try`. Here it got an unexpected type of error, but still shows the same `"JSON Error"` message. That's wrong and also makes the code more difficult to debug.
|
||||
|
||||
Fortunately, we can find out which error we've got, for instance by its `name`:
|
||||
|
||||
```js run
|
||||
try {
|
||||
|
||||
// ...
|
||||
JSON.papaparse(); // JSON.papaparse is not a function
|
||||
|
||||
} catch(e) {
|
||||
*!*
|
||||
alert(e.name); // "TypeError" for trying to call undefined property
|
||||
*/!*
|
||||
}
|
||||
```
|
||||
|
||||
The rule is simple:
|
||||
|
||||
**Catch should only process errors that it knows and throw all others.**
|
||||
|
||||
The technique is called "rethrowing":
|
||||
|
||||
1. Catch gets all errors.
|
||||
2. In `catch(e) {...}` block we analyze the error object `e`.
|
||||
2. If we don't know how to handle it, then do `throw e`.
|
||||
|
||||
In the code below, `catch` only handles `SyntaxError`:
|
||||
|
||||
```js run
|
||||
let json = '{ "age": 30 }'; // incomplete data
|
||||
try {
|
||||
|
||||
let user = JSON.parse(json);
|
||||
|
||||
if (!user.name) {
|
||||
throw new SyntaxError("Incomplete data: no name");
|
||||
}
|
||||
|
||||
*!*
|
||||
blabla(); // unexpected error
|
||||
*/!*
|
||||
|
||||
alert( user.name );
|
||||
|
||||
} catch(e) {
|
||||
|
||||
*!*
|
||||
if (e.name == "SyntaxError") {
|
||||
alert( "JSON Error: " + e.message );
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
*/!*
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The error made inside `catch` block "falls out" of `try..catch` and can be either caught by an outer `try..catch` construct (if exists) or kills the script.
|
||||
|
||||
The example below demonstrates how such errors can be caught by one more level of `try..catch`:
|
||||
|
||||
```js run
|
||||
function readData() {
|
||||
let json = '{ "age": 30 }';
|
||||
|
||||
try {
|
||||
// ...
|
||||
*!*
|
||||
blabla(); // error!
|
||||
*/!*
|
||||
} catch (e) {
|
||||
// ...
|
||||
if (e.name != 'SyntaxError') {
|
||||
*!*
|
||||
throw e; // rethrow (don't know how to deal with it)
|
||||
*/!*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
readData();
|
||||
} catch (e) {
|
||||
*!*
|
||||
alert( "External catch got: " + e ); // caught it!
|
||||
*/!*
|
||||
}
|
||||
```
|
||||
|
||||
Here `readData` only knows how to handle `SyntaxError`, while the outer `try..catch` knows how to handle everything.
|
||||
|
||||
## try..catch..finally
|
||||
|
||||
Wait, that's not all.
|
||||
|
||||
The `try..catch` construct may have one more code clause: `finally`.
|
||||
|
||||
If it exists, it runs in all cases:
|
||||
|
||||
- after `try`, if there were no errors,
|
||||
- after `catch`, if there were errors.
|
||||
|
||||
The extended syntax looks like this:
|
||||
|
||||
```js
|
||||
*!*try*/!* {
|
||||
... try to execute the code ...
|
||||
} *!*catch*/!*(e) {
|
||||
... handle errors ...
|
||||
} *!*finally*/!* {
|
||||
... execute always ...
|
||||
}
|
||||
```
|
||||
|
||||
Try to run this?
|
||||
|
||||
```js run
|
||||
try {
|
||||
alert( 'try' );
|
||||
if (confirm('Make an error?')) BAD_CODE();
|
||||
} catch (e) {
|
||||
alert( 'catch' );
|
||||
} finally {
|
||||
alert( 'finally' );
|
||||
}
|
||||
```
|
||||
|
||||
The code has two variants:
|
||||
|
||||
1. If say answer "Yes" to error, then `try -> catch -> finally`.
|
||||
2. If say "No", then `try -> finally`.
|
||||
|
||||
The `finally` clause is often used when we start doing something before `try..catch` and want to finalize it in any case of outcome.
|
||||
|
||||
For instance, we want to measure time that a Fibonacci numbers function `fib(n)` takes. Naturally, we can start measuring before it runs and finish afterwards. But what is there's an error during the function call? In particular, the implementation of `fib(n)` in the code below returns an error for negative or non-integer numbers.
|
||||
|
||||
The `finally` clause is a great place to finish the measurements no matter how the function finishes.
|
||||
|
||||
```js run
|
||||
let num = +prompt("Enter a positive integer number?", 35)
|
||||
|
||||
let diff, result;
|
||||
|
||||
function fib(n) {
|
||||
if (n < 0 || Math.trunc(n) != n) {
|
||||
throw new Error("Must not be negative, and also an integer.");
|
||||
}
|
||||
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
|
||||
}
|
||||
|
||||
let start = Date.now();
|
||||
|
||||
try {
|
||||
result = fib(num);
|
||||
} catch (e) {
|
||||
result = 0;
|
||||
*!*
|
||||
} finally {
|
||||
diff = Date.now() - start;
|
||||
}
|
||||
*/!*
|
||||
|
||||
alert(result || "error occured");
|
||||
|
||||
alert( `execution took ${diff}ms` );
|
||||
```
|
||||
|
||||
Here `finally` guarantees that the time will be measured correctly in both situations -- in case of a successful execution of `fib` and in case of an error in it.
|
||||
|
||||
You can check that by running the code with `num=35` -- executes normally, `finally` after `try`, and then with `num=-1`, there will be an immediate error, an the execution will take `0ms`.
|
||||
|
||||
In other words, there may be two ways to exist from a function: either a `return` or `throw`. The `finally` handles them both.
|
||||
|
||||
|
||||
```smart header="Variables are local to try..catch..finally clauses"
|
||||
Please note that `result` and `diff` variables in the code above are declared *before* `try..catch`.
|
||||
|
||||
Otherwise, if `let` were made inside the `{...}` clause, it would only be visible inside of it.
|
||||
```
|
||||
|
||||
````smart header="`finally` and `return`"
|
||||
Finally clause works for *any* exit from `try..catch`. That includes the `return` directive.
|
||||
|
||||
In the example below, there's a `return` in `try`. In this case, `finally` is executed just before the control returns to the outer code.
|
||||
|
||||
```js run
|
||||
function func() {
|
||||
|
||||
try {
|
||||
return 1;
|
||||
|
||||
} catch (e) {
|
||||
/* ... */
|
||||
} finally {
|
||||
*!*
|
||||
alert( 'finally' );
|
||||
*/!*
|
||||
}
|
||||
}
|
||||
|
||||
alert( func() ); // first works alert from finally, and then this one
|
||||
```
|
||||
````
|
||||
|
||||
````smart header="`try..finally`"
|
||||
|
||||
The `try..finally` construct, without `catch` clause, is also useful. We apply it when we don't want to handle errors right here, but want to be sure that processes that we started are finalized.
|
||||
|
||||
```js
|
||||
function func() {
|
||||
// start doing something that needs completion
|
||||
try {
|
||||
// ...
|
||||
} finally {
|
||||
// complete that thing even if all dies
|
||||
}
|
||||
}
|
||||
```
|
||||
In the code above, an error inside `try` always falls out, because there's no `catch`. But `finally` triggers before that.
|
||||
````
|
||||
|
||||
## Global catch
|
||||
|
||||
```warn header="Environment-specific"
|
||||
The information from this section is not a part of the core Javascript.
|
||||
```
|
||||
|
||||
Let's imagine we've got a fatal error outside of `try..catch`, and the script died.
|
||||
|
||||
Is there a way to react on it? We may want to log the error, show something to the user (normally he doesn't see the error message) etc.
|
||||
|
||||
There is none in the specification, but environments usually provide it, because it's really handy. For instance, Node.JS has [process.on('uncaughtException')](https://nodejs.org/api/process.html#process_event_uncaughtexception) for that. And in the browser we can assign a function to special [window.onerror](mdn:api/GlobalEventHandlers/onerror) property. It will run in case of an uncaught error.
|
||||
|
||||
The syntax:
|
||||
|
||||
```js
|
||||
window.onerror = function(message, url, line, col, error) {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
`message`
|
||||
: Error message.
|
||||
|
||||
`url`
|
||||
: URL of the script where error happened.
|
||||
|
||||
`line`, `col`
|
||||
: Line and column numbers where error happened.
|
||||
|
||||
`error`
|
||||
: Error object.
|
||||
|
||||
For instance:
|
||||
|
||||
```html run untrusted refresh height=1
|
||||
<script>
|
||||
*!*
|
||||
window.onerror = function(message, url, line, col, error) {
|
||||
alert(`${message}\n At ${line}:${col} of ${url}`);
|
||||
};
|
||||
*/!*
|
||||
|
||||
function readData() {
|
||||
badFunc(); // wops, something went wrong!
|
||||
}
|
||||
|
||||
readData();
|
||||
</script>
|
||||
```
|
||||
|
||||
The role of the global handler is usually not to recover the script execution -- that's probably impossible, but to send the error message to developers.
|
||||
|
||||
There are also web-services that provide error-logging facilities, like <https://errorception.com> or <http://www.muscula.com>. They give a script with custom `window.onerror` function, and once inserted into a page, it reports about all errors it gets to their server. Afterwards developers can browse them and get notifications on email about fresh errors.
|
||||
|
||||
## Summary
|
||||
|
||||
The `try..catch` construct allows to handle runtime errors. It literally allows to try running the code and catch errors that may occur in it.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
try {
|
||||
// run this code
|
||||
} catch(err) {
|
||||
// if an error happened, then jump here
|
||||
// err is the error object
|
||||
} finally {
|
||||
// do in any case after try/catch
|
||||
}
|
||||
```
|
||||
|
||||
There may be no `catch` section or no `finally`, so `try..catch` and `try..finally` are also valid.
|
||||
|
||||
Error objects have following properties:
|
||||
|
||||
- `message` -- the human-readable error message.
|
||||
- `name` -- the string with error name (error constructor name).
|
||||
- `stack` (non-standard) -- the stack at the moment of error creation.
|
||||
|
||||
We can also generate our own errors using the `throw` operator. Technically, the argument of `throw` can be anything, but usually it's an error object inheriting from the built-in `Error` class. More on extending errors in the next chapter.
|
||||
|
||||
Rethrowing is a basic pattern of error handling: a `catch` block usually expects and knows how to handle the particular error type, so it should rethrow errors it doesn't know.
|
||||
|
||||
Even if we don't have `try..catch`, most environments allow to setup a "global" error handler to catch errors that "fall out". In-browser that's `window.onerror`.
|
|
@ -0,0 +1,16 @@
|
|||
```js run untrusted
|
||||
class FormatError extends SyntaxError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "FormatError";
|
||||
}
|
||||
}
|
||||
|
||||
let err = new FormatError("formatting error");
|
||||
|
||||
alert( err.message ); // formatting error
|
||||
alert( err.name ); // FormatError
|
||||
alert( err.stack ); // stack
|
||||
|
||||
alert( err instanceof SyntaxError ); // true
|
||||
```
|
18
1-js/08-error-handling/12-oop-errors/1-format-error/task.md
Normal file
18
1-js/08-error-handling/12-oop-errors/1-format-error/task.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Inherit from SyntaxError
|
||||
|
||||
Create an error `FormatError` that inherits from `SyntaxError`.
|
||||
|
||||
Usage example:
|
||||
```js
|
||||
let err = new FormatError("formatting error");
|
||||
|
||||
alert( err.message ); // formatting error
|
||||
alert( err.name ); // FormatError
|
||||
alert( err.stack ); // stack
|
||||
|
||||
alert( err instanceof SyntaxError ); // true
|
||||
```
|
281
1-js/08-error-handling/12-oop-errors/article.md
Normal file
281
1-js/08-error-handling/12-oop-errors/article.md
Normal file
|
@ -0,0 +1,281 @@
|
|||
# Custom errors, extending Error
|
||||
|
||||
When we develop our software, we need our own error classes. For network operations we may need `HttpError`, for database operations `DbError`, for searching operations `NotFoundError` and so on.
|
||||
|
||||
Our errors should inherit from with the basic `Error` class and have basic error properties like `message`, `name` and, preferably, `stack`. But they also may have other properties of their own, e.g. `HttpError` objects may have `statusCode` property, that is `404` for the "page not found" error.
|
||||
|
||||
Technically, we can use standalone classes for errors, because Javascript allows to use `throw` with any argument. But if we inherit from `Error`, then we can use `obj instanceof Error` check to identify error objects. So it's better to inherit from it.
|
||||
|
||||
As we build our application, our own errors naturally form a hierarchy, for instance `HttpTimeoutError` may inherit from `HttpError`. Examples will follow soon.
|
||||
|
||||
## Extending Error
|
||||
|
||||
As an example, let's create a function `readUser(json)` that should read JSON with user data. We are getting that data from a remote server or, maybe it may be altered by a visitor, or just for the sheer safety -- we should to be aware of possible errors in `json`.
|
||||
|
||||
Here's an example of how a valid `json` may look:
|
||||
```js
|
||||
let json = `{ "name": "John", "age": 30 }`;
|
||||
```
|
||||
|
||||
If the function receives malformed `json`, then it should throw `SyntaxError`. Fortunately, `JSON.parse` does exactly that.
|
||||
|
||||
...But if the `json` is correct, that doesn't mean it has all the data. For instance, if may not have `name` or `age`.
|
||||
|
||||
That's called "data validation" -- we need to ensure that the data has all the necessary fields. And if the validation fails, then throwing `SyntaxError` would be wrong, because the data is syntactically correct. So we should throw `ValidationError` -- the error object of our own with the proper message and, preferable, with additional information about the offending field.
|
||||
|
||||
Let's make the `ValidationError` class. But to better understand what we're extending -- here's the approximate code for built-in [Error class](https://tc39.github.io/ecma262/#sec-error-message):
|
||||
|
||||
```js
|
||||
class Error {
|
||||
constructor(message) {
|
||||
this.message = message;
|
||||
this.name = "Error"; // (different names for different built-in errors)
|
||||
this.stack = <sequence of nested calls>; // non-standard! most environments support it
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now let's inherit from it:
|
||||
|
||||
```js run untrusted
|
||||
class ValidationError extends Error {
|
||||
constructor(message) {
|
||||
super(message); // (1)
|
||||
this.name = "ValidationError"; // (2)
|
||||
}
|
||||
}
|
||||
|
||||
function test() {
|
||||
throw new ValidationError("Wops!");
|
||||
}
|
||||
|
||||
try {
|
||||
test();
|
||||
} catch(err) {
|
||||
alert(err.message); // Wops!
|
||||
alert(err.name); // ValidationError
|
||||
alert(err.stack); // a list of nested calls with line numbers for each
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
1. In the line `(1)` we call the parent constructor to set the message. Javascript requires us to call `super` in the child constructor anyway.
|
||||
2. The `name` property for our own errors should be assigned manually, otherwise it would be set by the superclass (to `"Error"`).
|
||||
|
||||
|
||||
Let's try to use it in `readUser(json)`:
|
||||
|
||||
```js run
|
||||
class ValidationError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
function readUser(json) {
|
||||
let user = JSON.parse(json);
|
||||
|
||||
if (!user.age) {
|
||||
throw new ValidationError("No field: age");
|
||||
}
|
||||
if (!user.name) {
|
||||
throw new ValidationError("No field: name");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// Working example with try..catch
|
||||
|
||||
try {
|
||||
let user = readUser('{ "age": 25 }');
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
*!*
|
||||
alert("Invalid data: " + err.message); // Invalid data: No field: name
|
||||
*/!*
|
||||
} else if (err instanceof SyntaxError) {
|
||||
alert("JSON Syntax Error: " + err.message);
|
||||
} else {
|
||||
throw err; // unknown error, rethrow it
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Everything works -- both our `ValidationError` and the built-in `SyntaxError` from `JSON.parse` are correctly handled.
|
||||
|
||||
Please note how the code check for the error type in `catch (err) { ... }`. We could use `if (err.name == "ValidationError")`, but `if (err instanceof ValidationError)` is much better, because in the future we are going to extend `ValidationError`, make new subtypes of it, namely `PropertyRequiredError`. And `instanceof` check will continue to work. So that's future proof.
|
||||
|
||||
Also it's important that if we meet an unknown error, then we just rethrow it. The `catch` only knows how to handle validation and syntax errors, other kinds (due to a typo in the code or such) should fall through.
|
||||
|
||||
## Further inheritance
|
||||
|
||||
The `ValidationError` class is very generic. Let's make a more concrete class `PropertyRequiredError`, exactly for the absent properties. It will carry additional information about the property that's missing.
|
||||
|
||||
```js run
|
||||
class ValidationError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
}
|
||||
|
||||
*!*
|
||||
class PropertyRequiredError extends ValidationError {
|
||||
constructor(property) {
|
||||
super("No property: " + property);
|
||||
this.name = "PropertyRequiredError";
|
||||
this.property = property;
|
||||
}
|
||||
}
|
||||
*/!*
|
||||
|
||||
// Usage
|
||||
function readUser(json) {
|
||||
let user = JSON.parse(json);
|
||||
|
||||
if (!user.age) {
|
||||
throw new PropertyRequiredError("age");
|
||||
}
|
||||
if (!user.name) {
|
||||
throw new PropertyRequiredError("name");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// Working example with try..catch
|
||||
|
||||
try {
|
||||
let user = readUser('{ "age": 25 }');
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
*!*
|
||||
alert("Invalid data: " + err.message); // Invalid data: No property: name
|
||||
alert(err.name); // PropertyRequiredError
|
||||
alert(err.property); // name
|
||||
*/!*
|
||||
} else if (err instanceof SyntaxError) {
|
||||
alert("JSON Syntax Error: " + err.message);
|
||||
} else {
|
||||
throw err; // unknown error, rethrow it
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The new class `PropertyRequiredError` is easier to use, because we just pass the property name to it: `new PropertyRequiredError(property)`. The human-readable `message` is generated by the constructor.
|
||||
|
||||
Plese note that `this.name` in `PropertyRequiredError` once again assigned manually. We could evade that by using `this.constructor.name` for `this.name` in the superclass.
|
||||
|
||||
The generic solution would be to make `MyError` class that takes care of it, and inherit from it.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
class MyError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
*!*
|
||||
this.name = this.constructor.name;
|
||||
*/!*
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends MyError { }
|
||||
|
||||
class PropertyRequiredError extends ValidationError {
|
||||
constructor(property) {
|
||||
super("No property: " + property);
|
||||
this.property = property;
|
||||
}
|
||||
}
|
||||
|
||||
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
|
||||
```
|
||||
|
||||
|
||||
## Wrapping exceptions
|
||||
|
||||
The purpose of the function `readUser` in the code above is -- to "read the user data", right? There may occur different kinds of errors in the process, not only `SyntaxError` and `ValidationError`, but probably others if we continue developing it.
|
||||
|
||||
Right now the code which calls `readUser` uses multiple `if` in `catch` to check for different error types and rethrow if the error is unknown.
|
||||
|
||||
But the important questions is: do we want to check for all these types every time we call `readUser`?
|
||||
|
||||
Often the answer is: "No". The outer code wants to be "one level above all that". It wants to have some kind of "data reading error", and why exactly it happened -- is usually irrelevant (the message has the info). Or, even better if there is a way to get more details, but only if it wants to.
|
||||
|
||||
In our case, when a data-reading error occurs, we will create an object of the new class `ReadError`, that will provide the proper message. And we'll also keep the original error in its `cause` property, just in case.
|
||||
|
||||
```js run
|
||||
class ReadError extends Error {
|
||||
constructor(message, cause) {
|
||||
super(message);
|
||||
this.cause = cause;
|
||||
this.name = 'ReadError';
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends Error { /*...*/ }
|
||||
class PropertyRequiredError extends ValidationError { /* ... */ }
|
||||
|
||||
function validateUser(user) {
|
||||
if (!user.age) {
|
||||
throw new PropertyRequiredError("age");
|
||||
}
|
||||
|
||||
if (!user.name) {
|
||||
throw new PropertyRequiredError("name");
|
||||
}
|
||||
}
|
||||
|
||||
function readUser(json) {
|
||||
let user;
|
||||
|
||||
try {
|
||||
user = JSON.parse(json);
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
throw new ReadError("Syntax Error", err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
validateUser(user);
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
throw new ReadError("Validation Error", err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
readUser('{bad json}');
|
||||
} catch (e) {
|
||||
if (e instanceof ReadError) {
|
||||
*!*
|
||||
alert(e);
|
||||
// Original error: SyntaxError: Unexpected token b in JSON at position 1
|
||||
alert("Original error: " + e.cause);
|
||||
*/!*
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The approach is called "wrapping exceptions", because we take "low level exceptions" and "wrap" them into `ReadError` that is more abstract and more convenient to use for the calling code.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
- We can inherit from `Error` and other built-in error classes normally, just need to take care of `name` property and don't forget to call `super`.
|
||||
- Most of the time, we should use `instanceof` to check for particular errors. It also works with inheritance. But sometimes we have an error object coming from the 3rd-party library and there's no easy way to get the class. Then `name` property can be used for such checks.
|
||||
- Wrapping exceptions is a widespread technique when a function handles low-level exceptions and makes a higher-level object to report about the errors. Low-level exceptions sometimes become properties of that object like `err.cause` in the examples above, but that's not strictly required.
|
1
1-js/08-error-handling/index.md
Normal file
1
1-js/08-error-handling/index.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Error handling
|
Loading…
Add table
Add a link
Reference in a new issue