In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-02-20 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/02 Report--
This article mainly explains "what front-end exception handling". The content in the article is simple and clear, and it is easy to learn and understand. let's follow the editor's train of thought to study and learn "what front-end exception handling"!
What is an anomaly?
To use straightforward words to explain the exception is that something unexpected has happened to the program, which affects the correct operation of the program.
Fundamentally speaking, an exception is a data structure that stores information about the occurrence of the exception, such as error codes, error messages, and so on. Take the standard built-in object Error in JS as an example, its standard properties are name and message. However, different browser vendors have their own custom properties, which are not common. Mozilla browsers, for example, have added attributes such as filename and stack.
It is worth noting that only when an error is thrown will an exception be generated, and an error that is not thrown will not produce an exception. For example:
Function t () {console.log ("start"); new Error (); console.log ("end");} t ()
(animated demo)
This code does not generate any exceptions and the console does not have any error output.
Classification of anomalies
According to whether the program was running when the exception was generated, we can divide errors into compile-time exceptions and run-time exceptions.
A compile-time exception refers to an exception that occurs before the source code is compiled into executable code. The runtime exception refers to the exception that occurs after the executable code is loaded into memory for execution.
Compile-time exception
We know that TS will eventually be compiled into JS and executed in JS Runtime. Now that there is compilation, it is possible that the compilation will fail, and there will be compile-time exceptions.
For example, I wrote the following code using TS:
Const s: string = 123
This is obviously the wrong code. I declared the string type to s, but assigned it a value of number.
When I try to compile this file using tsc (typescript compilation tool, typescript compiler), an exception is thrown:
Tsc a.ts a.ts:1:7-error TS2322: Type '123' is not assignable to type 'string'. 1 const s: string = 123; ~ Found 1 error.
This exception is a compile-time exception because my code hasn't been executed yet.
However, it is not because you use TS that there are compile-time exceptions. JS also has compile-time exceptions. Some people may ask, is JS not an explanatory language? Is to explain while executing, there is no compilation link, how can there be a compile-time exception?
Don't worry, I'll give you an example and you'll understand. The code is as follows:
Function t () {console.log ('start') await sa console.log (' end')} t ()
The above code does not compile because of syntax errors, so it does not print start, which proves to be a compile-time exception. Although JS is an interpreted language, there is still a compilation phase, which is inevitable, so there will naturally be compilation exceptions.
In general, compilation exceptions can be found before the code is compiled into the final code, so it does less harm to us. Next, take a look at the daunting runtime exceptions.
Runtime exception
I'm sure you're very familiar with the runtime. This is probably the most common type of exception encountered at the front end. The well-known NPE (Null Pointer Exception) is the runtime exception.
Modify the above example slightly to get the following code:
Function t () {console.log ("start"); throw 1; console.log ("end");} t ()
(animated demo)
Notice that end does not print and t does not pop up the stack. In fact, t will eventually be popped up, just not like a normal return.
As above, start will be printed out. Because the exception is thrown while the code is running, this exception is a run-time exception. Such exceptions are more difficult to find than compile-time exceptions. The above example may be simple, but what if my exception is hidden in a process control statement (such as if else)? The program may enter the if statement that throws an exception on the customer's computer and another on your computer. This is the famous "good on my computer" incident.
Abnormal propagation
The propagation of exceptions is very similar to the browser event model I wrote earlier. But that is the data structure such as DOM, this is the data structure of function call stack, and event propagation has capture phase, exception propagation is not. In different C languages, exception propagation in JS is automatic and does not require programmers to manually pass it layer by layer. If an exception is not catch, it propagates layer by layer along the function call stack until the stack is empty.
There are two keywords in exception handling, throw (to throw an exception) and catch (to handle an exception). When an exception is thrown, the propagation of the exception begins. The exception propagates until the first catch is encountered. If the programmer does not have a manual catch, the program generally throws a similar unCaughtError, indicating that an exception has occurred and that the exception has not been handled by any catch language in the program. Uncaught exceptions are usually printed on the console with detailed stack information to help programmers troubleshoot problems quickly. In fact, the goal of our program is to avoid exceptions like unCaughtError, not general exceptions.
A little premise.
Because the Error object of JS has no code attribute, it can only be rendered according to message, which is not very convenient. I have made a simple extension here, and I use my own extended Error instead of native JS Error in many places, so I won't repeat it.
OldError = Error; Error = function ({code, message, fileName, lineNumber}) {error = new oldError (message, fileName, lineNumber); error.code = code; return error;}
Manual throw or automatic throw
Exceptions can be thrown either manually by the programmer or automatically by the program.
Throw new Error (`Icepm Exception`)
(examples thrown manually)
A = null; a.toString (); / / Thrown: TypeError: Cannot read property 'toString' of null
(an example that the program automatically throws)
It is easy to understand that an exception is automatically thrown. After all, which of our programmers has not seen an exception automatically thrown by a program?
"this anomaly pops up all of a sudden! you scared me!" , said an unknown programmer.
So when should an exception be thrown manually?
One guiding principle is that you have predicted that the program will not proceed correctly. For example, if we want to achieve division, the first thing we have to consider is the case where the divisor is 0. What should we do when the divisor is 0? Does it throw an exception, or does return have a special value? The answer is either, as long as you can distinguish it yourself, there is no strict reference standard. Let's take a look at throwing an exception and tell the caller your input. I can't handle this.
Function divide (a, b) {a = + a; b = + b; / / convert to digital if (! b) {/ / match + 0,-0, NaN throw new Error ({code: 1, message: "Invalid dividend" + b,}) } if (Number.isNaN (a)) {/ / match NaN throw new Error ({code: 2, message: "Invalid divisor" + a,});} return a / b;}
The above code throws an exception in two cases, telling the caller that I can't handle your input. Because both exceptions are automatically and manually thrown by the programmer, they are predictable exceptions.
As I just said, we can also distinguish abnormal input by returning values. Let's take a look at what the return value input is and what it has to do with the exception.
Exception or returns
If it is based on the exception form (an exception is thrown when an input that cannot be processed is encountered). When other code calls divide, it needs to catch itself.
Function t () {try {divide ("foo", "bar");} catch (err) {if (err.code = 1) {return console.log ("Divisor must be a number other than 0");} if (err.code = 2) {return console.log ("Divisor must be numeric");} throw new Error ("unpredictable error") }}
However, as I said above, divide functions can be designed without exceptions and can be distinguished by return values.
Function divide (a, b) {a = + a; b = + b; / / convert to digital if (! b) {/ / match + 0,-0, NaN return new Error ({code: 1, message: "Invalid dividend" + b,}) } if (Number.isNaN (a)) {/ / match NaN return new Error ({code: 2, message: "Invalid divisor" + a,});} return a / b;}
Of course, the way we use it should be changed accordingly.
Function t () {const res = divide ("foo", "bar"); if (res.code = 1) {return console.log ("the divisor must be a number other than 0");} if (res.code = 2) {return console.log ("the divisor must be a number");} return new Error ("unpredictable error");}
This function design is functionally the same as the one that throws an exception, except that it tells the caller in a different way. If you choose the second way instead of throwing an exception, you actually need the caller to write additional code to distinguish between normal and abnormal situations, which is not a good programming habit.
However, in languages such as Go, where the return value can be plural, we do not need to use the above poor method, but can:
Res, err: = divide ("foo", "bar"); if err! = nil {log.Fatal (err)}
This is different from try catch, which is used in languages such as Java and JS. Go handles exceptions through the panic recover defer mechanism. If you are interested, you can check out the error testing section of the Go source code.
Maybe you are not familiar with Go. It doesn't matter. Let's take a look at shell again. In fact, shell also handles exceptions by returning values, and we can use & dollar;? Getting the return value of the previous command is essentially a propagation of the call stack, and the exception is handled by returning the value rather than catching it.
As a function return value, like try catch, this is a matter decided jointly by the designers and developers of the language.
It is mentioned above that exception propagation acts on the function call stack. When an exception occurs, it returns layer by layer along the function call stack until the first catch statement. Of course, exceptions can still be triggered inside the catch statement (automatically or manually). If an exception occurs inside the catch statement, the above logic continues along its function call stack, the technical term is stack unwinding.
In fact, not all languages can do stack unwinding, which we will be able to recover in the next "can run-time exceptions be recovered?" "part of the explanation.
Pseudo code to describe:
Function bubble (error, fn) {if (fn.hasCatchBlock ()) {runCatchCode (error);} if (callstack.isNotEmpty ()) {bubble (error, callstack.pop ());}}
From my pseudo code, we can see that the so-called stack unwinding is actually callstack.pop ().
That's what abnormal spread is all about! That's all.
Exception handling
We already know about the unusual mode of transmission. So the next question is, how should we handle exceptions in this propagation process?
Let's look at a simple example:
Function a () {b ();} function b () {c ();} function c () {throw new Error ("an error occured");} a ()
We put the above code into chrome to execute, and the output is displayed on the console as follows:
We can clearly see the calling relationship of the function. That is, the error occurs in c, while c is called by b, and b is called by a. This function call stack exists to make it easy for developers to locate the problem.
In the above code, we have no catch errors, so there is uncaught Error on it.
So what will happen if we catch? How does the location of the catch affect the results? Is the effect of catch the same in a, b, and c?
Let's take a look at this separately:
Function a () {b ();} function b () {c ();} function c () {try {throw new Error ("an error occured");} catch (err) {console.log (err);}} a ()
(catch in c)
We put the above code into chrome to execute, and the output is displayed on the console as follows:
As you can see, there is no uncaught Error at this time, only the standard output is displayed on the console, not the error output (because I am using console.log, not console.error). More importantly, however, if we do not have catch, then the later synchronization code will not be executed.
For example, add a line of code under the throw of c, which cannot be executed, regardless of whether the error is caught or not.
Function c () {try {throw new Error ("an error occured"); console.log ("will never run");} catch (err) {console.log (err);}}
Let's move catch to pilot b to try it.
Function a () {b ();} function b () {try {c ();} catch (err) {console.log (err);}} function c () {throw new Error ("an error occured");} a ()
(catch in b)
In this example, it is not fundamentally different from the capture in c above. In fact, it is the same to capture in a, no longer paste the code here, and try it for yourself if you are interested.
Since the function at the top of the function call stack reports an error, any function below the function call stack can be captured, and the effect is not essentially different. So the question is, where on earth should I handle the error?
The answer is the chain of responsibility model. Let's start with a brief introduction to the chain of responsibility model, but the details will not be expanded here.
If lucifer asks for leave.
If the number of days off is less than or equal to 1 day, the supervisor can agree.
If the leave is greater than 1 day, but less than or equal to 3 days, CTO consent is required.
If the number of days off is more than three days, the boss's consent is required.
This is a typical chain of responsibility model. It is certain who has the responsibility to do things, and don't do things beyond your ability. For example, the supervisor should not agree to the approval of more than 1 day.
For example, suppose our application has three exception handling classes: user input error, network error, and type error. The following code incorrectly reports a user input exception when the code is executed. This exception is not caught by C, it will unwind stack to b, and after the catch in b reaches this error, it is determined that it can be handled by looking at the code value, so I can handle this is printed.
Function a () {try {b ();} catch (err) {if (err.code = "NETWORK_ERROR") {return console.log ("I can handle this");} / / can't handle, pass it down throw err;}} function b () {try {c () } catch (err) {if (err.code = "INPUT_ERROR") {return console.log ("I can handle this");} / / can't handle, pass it down throw err;}} function c () {throw new Error ({code: "INPUT_ERROR", message: "an error occured",});} a ()
If other exceptions are thrown in c, such as network exceptions, then b cannot be handled. Although b catch resides, it is a good idea to continue to throw exceptions instead of swallowing them because you cannot handle them. Don't be afraid of mistakes, throw them out. Only exceptions that are not caught are scary, and if an error can be caught and handled correctly, it is not scary.
For example:
Function a () {try {b ();} catch (err) {if (err.code = "NETWORK_ERROR") {return console.log ("I can handle this");} / / can't handle, pass it down throw err;}} function b () {try {c () } catch (err) {if (err.code = "INPUT_ERROR") {return console.log ("I can handle this");} function c () {throw new Error ({code: "NETWORK_ERROR", message: "an error occured",});} a ()
As in the above code, no exception will be thrown and it will be completely swallowed up, which is simply a disaster for us to debug the problem. So remember not to engulf exceptions you can't handle. The right thing to do is to catch only the exceptions you can handle and throw the exceptions you can't handle, which is a typical application of the chain of responsibility pattern.
This is just a simple example, enough to go around for half a day. The actual business must be much more complicated than this. Therefore, exception handling is definitely not an easy task.
If who handles the exception is a difficult thing, then it is even more difficult to decide who handles the exception in asynchrony. Let's take a look.
Synchronous and asynchronous
Synchronization and asynchrony have always been difficult for the front end to overcome, and the same is true for exception handling. Take the read file API which is widely used in NodeJS as an example. It has two versions, one is asynchronous and the other is synchronous. Synchronous reads should only be used when it cannot proceed without this file. Such as reading a configuration file. Instead of reading a picture on the user's disk in a browser, for example, it will block the main thread and cause the browser to get stuck.
/ / asynchronously read the file fs.readFileSync (); / / synchronously read the file fs.readFile ()
When we try to read a file that does not exist synchronously, the following exception is thrown:
Fs.readFileSync ('something-not-exist.lucifer'); console.log (' frontal cavity'); Thrown: Error: ENOENT: no such file or directory, open' something-not-exist.lucifer' at Object.openSync (fs.js:446:3) at Object.readFileSync (fs.js:348:35) {errno:-2, syscall: 'open', code:' ENOENT', path: 'something-not-exist.lucifer'}
And the front end of the brain will not be printed. This is easier to understand. We have already explained it above.
And if you do it asynchronously:
Fs.readFile ('something-not-exist.lucifer', (err, data) = > {if (err) {throw err}}); console.log (' lucifer') lucifer undefined Thrown: [Error: ENOENT: no such file or directory, open' something-not-exist.lucifer'] {errno:-2, code: 'ENOENT', syscall:' open', path: 'something-not-exist.lucifer'} >
The front end of the brain will be printed.
The essence is that the function call to fs.readFile is successful and returns from the call stack and executes to the console.log ('lucifer') on the next line. So when the error occurs, the call stack is empty, as can be seen in the error stack information above.
Students who don't understand why the call stack is empty can take a look at my previous article "understanding browser event loops".
The purpose of try catch is simply to catch errors in the current call stack (as discussed in the exception propagation section above). So asynchronous errors cannot be caught, such as
Try {fs.readFile ("something-not-exist.lucifer", (err, data) = > {if (err) {throw err;}});} catch (err) {console.log ("catching an error");}
The catching an error above will not be printed. Because this catch statement is not included in the call stack when the error is thrown, but only when the fs.readFile is executed.
If we switch to the example of reading files synchronously, take a look:
Try {fs.readFileSync ("something-not-exist.lucifer");} catch (err) {console.log ("catching an error");}
The above code prints the catching an error. Because the read file is initiated synchronously, the thread is suspended before the file is returned, and when the thread resumes execution, the fs.readFileSync is still in the function call stack, so the exception generated by fs.readFileSync will bubble into the catch statement.
To put it simply, errors generated asynchronously cannot be caught with try catch, but with callbacks.
Some people may ask, I have seen using try catch to catch asynchronous exceptions. For example:
RejectIn = (ms) = > new Promise ((_, r) = > {setTimeout (() = > {r (1);}, ms);}); async function t () {try {await rejectIn (0);} catch (err) {console.log ("catching an error", err);}} t ()
In essence, this is just a grammatical candy, a grammatical candy of Promise.prototype.catch. The reason why this grammatical sugar can be established is that it uses the packaging type of Promise. If you do not use the wrapper type, for example, the above fs.readFile is not packaged with Promise and other wrapper types, you can't capture it with try catch.
If we use babel escape, we will find that try catch is missing and becomes a switch case statement. That's why try catch "catches asynchronous exceptions," that's all, and nothing more.
(babel escape result)
The babel escape environment I use is recorded here, you can click on the link directly.
Although browsers are not as implemented as babel escapes, at least we understand one thing. The current mechanism of try catch is unable to catch asynchronous exceptions.
Container wrappers, such as Promise, are recommended for asynchronous error handling. Then use catch for processing. In fact, there are many similarities between Promise's catch and try catch's catch, which can be compared to the past.
Like synchronization, many principles are universal. For example, do not swallow the exception asynchronously. The following code is bad because it engulfs exceptions it cannot handle.
P = Promise.reject (1); p.catch (() = > {})
A more appropriate approach would be something like this:
P = Promise.reject (1); p.catch ((err) = > {if (err = = 1) {return console.log ("I can handle this");} throw err;})
Is it possible to completely eliminate runtime exceptions?
One of my biggest headaches about the current front-end situation is that people rely too much on the runtime and ignore the compile time. I've seen a lot of programs, and if you don't run it, you have no idea how the program goes and what the shape of each variable is. No wonder console.log can be seen everywhere. I'm sure you feel the same way. Maybe you are the one who wrote this kind of code, maybe you are the one who wipes other people's buttocks. Why is this? It's because people rely too much on the runtime. The advent of TS greatly improves this, as long as you are using typescript instead of anyscript. In fact, eslint and stylint also contribute to this, after all, they are static analysis tools.
I strongly recommend keeping exceptions at compile time, not at run time. Take it to the extreme: if all exceptions occur at compile time, they certainly don't occur at run time. So can we restructure the application with confidence?
Fortunately, we can do it. However, if the current language can not do so, then the existing language system needs to be reformed. The cost of this kind of transformation is really high. Not only API, but also the programming model has undergone earth-shaking changes, otherwise functions would not have been unpopular for so many years.
Those who are not familiar with functional programming can take a look at the introduction to functional programming that I wrote earlier.
What if the anomaly can be completely eliminated? Before answering this question, let's take a look at elm, a language that claims to be free of runtime exceptions. Elm is a functional programming language that can be compiled into JS, which encapsulates side effects such as network IO, and is a declarative deductible language. Interestingly, elm also has exception handling. The section on exception handling (Error Handling) in elm has two sections: Maybe and Result. One of the reasons elm doesn't have runtime exceptions is because of them. To sum up "Why there are no exceptions in elm", that is, elm regards exceptions as data.
Take a simple example:
MaybeResolveOrNot = (ms) = > setTimeout (() = > {if (Math.random () > 0.5) {console.log ("ok");} else {throw new Error ("error");}})
Half of the above code may report an error. Then this is not allowed to happen in elm. All code that may cause an exception is forced to wrap a container, which in this case is Maybe.
Names may be different in other functional programming languages, but they have the same meaning. In fact, not only the exception, but also the normal data will be packaged into the container, and you need to get the data through the container's interface. If it's hard to understand, you can simply think of it as Promsie (but not exactly equivalent).
Maybe may return normal data data, or it may generate an error error. It can only be one of them at some point, and it's only when it's running that we really know what it is. From this point of view, it looks a bit like Schrodinger's cat.
However, Maybe has fully taken into account the existence of exceptions, and everything is under its control. All exceptions can be deduced at compile time. Of course, in order to deduce these things, you need to encapsulate the entire programming model and abstract it. For example, DOM cannot be used directly, but requires an intermediate layer.
Let's take a look at a more common example, NPE:
Null.toString ()
Elm won't happen either. The reason is also simple, because null is also wrapped, and when you access it through this wrapper type, the container has the ability to avoid this situation, so there are no exceptions. Of course, a very important premise here is that it can be deduced, and this is the characteristic of functional programming languages. This part is beyond the scope of this article and will not be discussed here.
Can run-time exceptions be recovered?
The last topic to be discussed is whether runtime exceptions can be recovered. Let's first explain what a run-time abnormal recovery is. Or use the above example:
Function t () {console.log ("start"); throw 1; console.log ("end");} t ()
We already know this. End will not print. Even though it doesn't help if you write this:
Function t () {try {console.log ("start"); throw 1; console.log ("end");} catch (err) {console.log ("relax, I can handle this");}} t ()
What if I want it to print? I want to let the program face the exception can do their own recover? I've caught this error, and I'm sure I can handle it, so let the process go! If you have the ability to do this, this is run-time exception recovery.
I regret to tell you that, as far as I know, no engine can do this at present.
This example is too simple to help us understand what runtime exception recovery is, but not enough to show us how useful it is.
Let's look at a more complex example, where we directly use the function divide implemented above.
Function t () {try {const res = divide ("foo", "bar"); alert (`you got ${res} `);} catch (err) {if (err.code = 1) {return console.log ("Divisor must be a number other than 0");} if (err.code = 2) {return console.log ("Divisor must be numeric") } throw new Error ("unpredictable error");}
As in the code above, you will enter catch instead of alert. So for the user, the application is unresponsive. This is unacceptable.
One thing to complain about is that this kind of thing is really common, it's just that people don't use alert.
If only our code could continue to return to the location of the error and continue to execute after entering catch.
How to achieve the recovery of abnormal interrupts? As I just said: as far as I know, no engine can recover abnormally. Then I will invent a new grammar to solve this problem.
Function t () {try {const res = divide ("foo", "bar"); alert (`you got ${res} `);} catch (err) {console.log ("releax, I can handle this"); resume-1;}} t ()
The above resume is a keyword I defined, and the function is to return to the place where the exception occurred if an exception is encountered, and then give the function that currently has the exception a return value of-1, so that the subsequent code can run normally without being affected. This is actually a fallback.
This is definitely an advanced idea. Of course, the challenge is also very great, which has a great impact on the existing system, and a lot of things have to be changed. I hope the community will consider adding this to the standard.
Best practic
From the previous study, you already know what exceptions are, how they are generated, and how to handle them correctly (synchronous and asynchronous). Next, let's talk about best practices for exception handling.
We usually develop an application. If you look at it from the perspective of producers and consumers. When we use frameworks, libraries, modules, or even functions encapsulated by others, we are consumers. And when what we write is used by others, we are the producers.
In fact, even if there are multiple modules within the producer, there will be a re-identity transformation between the producer and the consumer. However, for the sake of simplicity, this relationship is not considered in this article. The producer here refers to the function for others to use, is the pure producer.
From this perspective, take a look at the best practices for exception handling.
As a consumer
As consumers, what we care about is whether the features used will throw exceptions, and if so, what exceptions they have. For example:
What are the exceptions to import foo from "lucifer"; try {foo.bar ();} catch (err) {/ /? }
Of course, in theory, foo.bar could produce any exception, regardless of what its API says. But we are concerned with predictable anomalies. So you must want to have an API document at this time detailing the possible exceptions to this API.
For example, the four possible exceptions to this foo.bar are A _ Magi B _ C and D respectively. Among them, An and B are something I can handle, while C and D are not. Then I should:
Import foo from "lucifer"; try {foo.bar ();} catch (err) {if (err.code = "A") {return console.log ("A happened");} if (err.code = "B") {return console.log ("B happened");} throw err;}
As you can see, whether it's C and D, or the various possible exceptions that are not listed in API, our approach is to throw them directly.
As a producer
If you are a producer, all you have to do is provide the detailed API mentioned above to tell consumers what your possible mistakes are. In this way, consumers can make corresponding judgments in catch and handle abnormal situations.
Thank you for your reading. the above is the content of "what front-end exception handling". After the study of this article, I believe you have a deeper understanding of what front-end exception handling is. The specific use of the situation also needs to be verified in practice. Here is, the editor will push for you more related knowledge points of the article, welcome to follow!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.