fixes
This commit is contained in:
parent
fb03c7d539
commit
2c57f11fdf
6 changed files with 79 additions and 44 deletions
|
@ -2,11 +2,11 @@
|
|||
|
||||
When we develop something, we often need our own error classes to reflect specific things that may go wrong in our tasks. For errors in network operations we may need `HttpError`, for database operations `DbError`, for searching operations `NotFoundError` and so on.
|
||||
|
||||
Our errors should inherit from basic `Error` class and support 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 with a value like `404` or `403` or `500`.
|
||||
Our errors should support 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 with a value like `404` or `403` or `500`.
|
||||
|
||||
Technically, we can use standalone classes for our errors, because JavaScript allows to use `throw` with any argument. But if we inherit from `Error`, then it becomes possible to use `obj instanceof Error` check to identify error objects. So it's better to inherit from it.
|
||||
JavaScript allows to use `throw` with any argument, so technically our custom error classes don't need to inherit from `Error`. But if we inherit, then it becomes possible to use `obj instanceof Error` 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.
|
||||
As we build our application, our own errors naturally form a hierarchy, for instance `HttpTimeoutError` may inherit from `HttpError`, and so on.
|
||||
|
||||
## Extending Error
|
||||
|
||||
|
@ -17,14 +17,20 @@ Here's an example of how a valid `json` may look:
|
|||
let json = `{ "name": "John", "age": 30 }`;
|
||||
```
|
||||
|
||||
If `JSON.parse` receives malformed `json`, then it throws `SyntaxError`. But even if `json` is syntactically correct, it may don't have the necessary data. For instance, if may not have `name` and `age` properties that are essential for our users.
|
||||
Internally, we'll use `JSON.parse`. If it receives malformed `json`, then it throws `SyntaxError`.
|
||||
|
||||
That's called "data validation" -- we need to ensure that the data has all the necessary fields. And if the validation fails, then it not really a `SyntaxError`, because the data is syntactically correct. Let's create `ValidationError` -- the error object of our own with additional information about the offending field.
|
||||
But even if `json` is syntactically correct, that doesn't mean that it's a valid user, right? It may miss the necessary data. For instance, if may not have `name` and `age` properties that are essential for our users.
|
||||
|
||||
Our `ValidationError` should inherit from the built-in `Error` class. 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):
|
||||
Our function `readUser(json)` will not only read JSON, but check ("validate") the data. If there are no required fields, or the format is wrong, then that's an error. And that's not a `SyntaxError`, because the data is syntactically correct, but another kind of error. We'll call it `ValidationError` and create a class for it. An error of that kind should also carry the information about the offending field.
|
||||
|
||||
Our `ValidationError` class should inherit from the built-in `Error` class.
|
||||
|
||||
That class is built-in, but we should have its approximate code before our eyes, to understand what we're extending.
|
||||
|
||||
So here you are:
|
||||
|
||||
```js
|
||||
// "pseudocode" for the built-in Error class defined by JavaScript itself
|
||||
// The "pseudocode" for the built-in Error class defined by JavaScript itself
|
||||
class Error {
|
||||
constructor(message) {
|
||||
this.message = message;
|
||||
|
@ -34,7 +40,7 @@ class Error {
|
|||
}
|
||||
```
|
||||
|
||||
Now let's inherit from it:
|
||||
Now let's go on and inherit `ValidationError` from it:
|
||||
|
||||
```js run untrusted
|
||||
*!*
|
||||
|
@ -59,10 +65,10 @@ try {
|
|||
}
|
||||
```
|
||||
|
||||
Please note:
|
||||
Please take a look at the constructor:
|
||||
|
||||
1. In the line `(1)` we call the parent constructor to set the message. JavaScript requires us to call `super` in the child constructor.
|
||||
2. The parent constructor sets the `name` property to `"Error"`, so here we reset it to the right value.
|
||||
1. In the line `(1)` we call the parent constructor. JavaScript requires us to call `super` in the child constructor, so that's obligatory. The parent constructor sets the `message` property.
|
||||
2. The parent constructor also sets the `name` property to `"Error"`, so in the line `(2)` we reset it to the right value.
|
||||
|
||||
Let's try to use it in `readUser(json)`:
|
||||
|
||||
|
@ -97,23 +103,34 @@ try {
|
|||
*!*
|
||||
alert("Invalid data: " + err.message); // Invalid data: No field: name
|
||||
*/!*
|
||||
} else if (err instanceof SyntaxError) {
|
||||
} else if (err instanceof SyntaxError) { // (*)
|
||||
alert("JSON Syntax Error: " + err.message);
|
||||
} else {
|
||||
throw err; // unknown error, rethrow it
|
||||
throw err; // unknown error, rethrow it (**)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Everything works -- both our `ValidationError` and the built-in `SyntaxError` from `JSON.parse` can be generated and handled.
|
||||
The `try..catch` block in the code above handles both our `ValidationError` and the built-in `SyntaxError` from `JSON.parse`.
|
||||
|
||||
Please take a look at how the code checks 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.
|
||||
Please take a look at how we use `instanceof` to check for the specific error type in the line `(*)`.
|
||||
|
||||
Also it's important that if `catch` meets an unknown error, then it rethrows 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.
|
||||
We could also look at `err.name`, like this:
|
||||
|
||||
```js
|
||||
// ...
|
||||
// instead of (err instanceof SyntaxError)
|
||||
} else if (err.name == "SyntaxError") { // (*)
|
||||
// ...
|
||||
```
|
||||
|
||||
The `instanceof` version is much better, because in the future we are going to extend `ValidationError`, make subtypes of it, like `PropertyRequiredError`. And `instanceof` check will continue to work for new inheriting classes. So that's future-proof.
|
||||
|
||||
Also it's important that if `catch` meets an unknown error, then it rethrows it in the line `(**)`. 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. Many things may be wrong. The property may be absent or it may be in a wrong format (like a string value for `age`). Let's make a more concrete class `PropertyRequiredError`, exactly for absent properties. It will carry additional information about the property that's missing.
|
||||
The `ValidationError` class is very generic. Many things may go wrong. The property may be absent or it may be in a wrong format (like a string value for `age`). Let's make a more concrete class `PropertyRequiredError`, exactly for absent properties. It will carry additional information about the property that's missing.
|
||||
|
||||
```js run
|
||||
class ValidationError extends Error {
|
||||
|
@ -168,9 +185,11 @@ try {
|
|||
|
||||
The new class `PropertyRequiredError` is easy to use: we only need to pass the property name: `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 make our own "basic error" class, name it `MyError` that removes this burden from our shoulders by using `this.constructor.name` for `this.name` in the constructor. And then inherit from it.
|
||||
Please note that `this.name` in `PropertyRequiredError` constructor is again assigned manually. That may become a bit tedius -- to assign `this.name = <class name>` when creating each custom error. But there's a way out. We can make our own "basic error" class that removes this burden from our shoulders by using `this.constructor.name` for `this.name` in the constructor. And then inherit from it.
|
||||
|
||||
Here we go:
|
||||
Let's call it `MyError`.
|
||||
|
||||
Here's the code with `MyError` and other custom error classes, simplified:
|
||||
|
||||
```js run
|
||||
class MyError extends Error {
|
||||
|
@ -195,17 +214,19 @@ class PropertyRequiredError extends ValidationError {
|
|||
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
|
||||
```
|
||||
|
||||
Now the inheritance became simpler, as we got rid of the `"this.name = ..."` line in the constructor.
|
||||
Now custom errors are much shorter, especially `ValidationError`, as we got rid of the `"this.name = ..."` line in the constructor.
|
||||
|
||||
## 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. Right now we have `SyntaxError` and `ValidationError`, but there may appear more if we put more stuff into it.
|
||||
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. Right now we have `SyntaxError` and `ValidationError`, but in the future `readUser` function may grow: the new code will probably generate other kinds of errors.
|
||||
|
||||
Right now the code which calls `readUser` uses multiple `if` in `catch` to check for different error types. The important questions is: do we really want to check for all error types one-by-one every time we call `readUser`?
|
||||
The code which calls `readUser` should handle these errors. Right now it uses multiple `if` in the `catch` block to check for different error types and rethrow the unknown ones. But if `readUser` function generates several kinds of errors -- then we should ask ourselves: do we really want to check for all error types one-by-one in every code that calls `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". 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 we need to.
|
||||
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". Why exactly it happened -- is often irrelevant (the error message describes it). Or, even better if there is a way to get error details, but only if we need to.
|
||||
|
||||
So let's make a new class `ReadError` to represent such errors. If an error occurs inside `readUser`, we'll catch it there and generate `ReadError`. We'll also keep the reference to the original error in the `cause` property.
|
||||
So let's make a new class `ReadError` to represent such errors. If an error occurs inside `readUser`, we'll catch it there and generate `ReadError`. We'll also keep the reference to the original error in the `cause` property. Then the outer code will only have to check for `ReadError`.
|
||||
|
||||
Here's the code that defines `ReadError` and demonstrates its use in `readUser` and `try..catch`:
|
||||
|
||||
```js run
|
||||
class ReadError extends Error {
|
||||
|
@ -273,7 +294,9 @@ try {
|
|||
}
|
||||
```
|
||||
|
||||
In the code above, `readUser` does exactly as described -- catches syntax and validation errors and throws `ReadError` errors instead (unknown errors are rethrown as usual).
|
||||
In the code above, `readUser` works exactly as described -- catches syntax and validation errors and throws `ReadError` errors instead (unknown errors are rethrown as usual).
|
||||
|
||||
So the outer code checks `instanceof ReadError` and that's it. No need to list possible all error types.
|
||||
|
||||
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. It is widely used in object-oriented programming.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue