Большинство наших проектов устроены так: когда во время работы программы возникает какая-то ошибка, то программа аварийно завершается. Иногда при этом она выдаёт сообщение об ошибке. Кажется, что это нормальная ситуация, но на самом деле большинство ошибок можно предусмотреть и научить программу правильно с ними работать. Для этого нам нужны обработчики ошибок.
Что такое обработчик ошибок
Чтобы программа знала, что делать, если возникла какая-то ошибка, используют обработчики исключительных ситуаций, или, проще говоря, обработчики исключений. Смысл такой:
- Мы заранее прикидываем, в каком месте и почему может возникнуть ошибка.
- Пишем в этом месте специальный код, который предупредит компьютер, что это плановая ошибка и что у нас уже есть решение, мол, всё под контролем.
- Компьютер применяет наше решение и переходит к следующей команде.
- Программа не падает, не завершается с ошибкой, а продолжает работать.
Такие обработчики есть не в каждом языке программирования, но большинство современных языков это умеют делать.
Пример программы без обработчика исключений
Допустим, у нас в программе на Python предусмотрено чтение данных из файла и есть такой код:
file = open("myfile2.txt")
Но если на диске этого файла не будет, то компьютер, когда дойдёт до этой строчки, выдаст ошибку:

Давайте нарисуем это в виде простой схемы:

Получается, что наша задача — предусмотреть вариант, что на диске не будет нужного файла, и придумать поведение программы в этом случае. Используем для этого обработчик исключений.
Программа с обработчиком исключений
Если мы знаем, что в каком-то месте возможна ошибка, то можем тогда предусмотреть этот сценарий и подстраховаться. Для этого используют обработчик и делают так:
- В том месте, где можно предусмотреть ошибку, делают специальный блок.
- В этом блоке запускают команду и смотрят, будет ошибка или нет.
- Если ошибки нет — программа работает дальше.
- Если возникла ошибка — выполнятся то, что написано в обработчике ошибок, а потом программа работает дальше.
В этой ситуации программа не зависнет и не вывалится с ошибкой, а сама сможет её обработать и делать дальше то, что нужно:

try:
file = open("myfile2.txt")
except FileNotFoundError:
print("Файл не найден, создаю новый")
file = open("myfile2.txt","a")
Команда try — это начало нашего обработчика исключений. Она говорит компьютеру: «Попробуй выполнить вот эту команду, а мы посмотрим, что произойдёт».
Except — это какую ошибку мы ожидаем здесь увидеть. В нашем случае мы хотим предусмотреть случай, что такого файла нет, поэтому пишем стандартную ошибку для такой ситуации.
👉 Сравните текст этой ошибки с тем, что нам выдал компьютер в предыдущем разделе.
В других языках конструкция обработчика исключений может выглядеть по-другому, но смысл тот же: говорим компьютеру, какую команду нужно выполнить и что делать, если появилась конкретная ошибка.
Когда что-то не предусмотрено — будет ошибка
Если программе в этом блоке встретится другая ошибка, не та, которую мы предусмотрели, то программа остановится и всё перестанет работать. Например, вот какие ошибки могут возникнуть с файлом:
- файл есть на диске, но к нему нет прав доступа;
- файл занят другой программой;
- сам диск повреждён и данные не читаются.
Во всех этих случаях программа сломается, потому что мы не предусмотрели эти ситуации:

Получается, всё нужно делать с обработкой исключений?
Нет, и вот почему:
- Обработка исключений занимает лишнее время, поэтому программа с ними работает медленнее, чем без них.
- Не всё можно предусмотреть. Если разработчик не знает, что здесь может быть ошибка, то и предусмотреть он это тоже не сможет.
- Конструкции с обработчиками делают код менее читаемым и понятным для человека. Ему нужно будет держать в голове точку начала обработки, понять, как обработка влияет на программу в целом, и выяснить, что будет с программой после работы обработчика ошибок.
Конечно, есть места в коде, которые лучше делать с обработкой ошибок: работа с файлами, сетевые запросы или получение внешних данных. Но запихивать исключения на каждую команду в программе точно не стоит.
In computing and computer programming, exception handling is the process of responding to the occurrence of exceptions – anomalous or exceptional conditions requiring special processing – during the execution of a program. In general, an exception breaks the normal flow of execution and executes a pre-registered exception handler; the details of how this is done depend on whether it is a hardware or software exception and how the software exception is implemented. Exception handling, if provided, is facilitated by specialized programming language constructs, hardware mechanisms like interrupts, or operating system (OS) inter-process communication (IPC) facilities like signals. Some exceptions, especially hardware ones, may be handled so gracefully that execution can resume where it was interrupted.
Definition[edit]
The definition of an exception is based on the observation that each procedure has a precondition, a set of circumstances for which it will terminate «normally».[1] An exception handling mechanism allows the procedure to raise an exception[2] if this precondition is violated,[1] for example if the procedure has been called on an abnormal set of arguments. The exception handling mechanism then handles the exception.[3]
The precondition, and the definition of exception, is subjective. The set of «normal» circumstances is defined entirely by the programmer, e.g. the programmer may deem division by zero to be undefined, hence an exception, or devise some behavior such as returning zero or a special «ZERO DIVIDE» value (circumventing the need for exceptions).[4] Common exceptions include an invalid argument (e.g. value is outside of the domain of a function), an unavailable resource (like a missing file, a hard disk error, or out-of-memory errors), or that the routine has detected a normal condition that requires special handling, e.g., attention, end of file.
Exception handling solves the semipredicate problem, in that the mechanism distinguishes normal return values from erroneous ones. In languages without built-in exception handling such as C, routines would need to signal the error in some other way, such as the common return code and errno pattern.[5] Taking a broad view, errors can be considered to be a proper subset of exceptions,[6] and explicit error mechanisms such as errno can be considered (verbose) forms of exception handling.[5] The term «exception» is preferred to «error» because it does not imply that anything is wrong — a condition viewed as an error by one procedure or programmer may not be viewed that way by another. Even the term «exception» may be misleading because its typical connotation of «outlier» indicates that something infrequent or unusual has occurred, when in fact raising the exception may be a normal and usual situation in the program.[7] For example, suppose a lookup function for an associative array throws an exception if the key has no value associated. Depending on context, this «key absent» exception may occur much more often than a successful lookup.[8]
A major influence on the scope and use of exceptions is social pressure, i.e. «examples of use, typically found in core libraries, and code examples in technical books, magazine articles, and online discussion forums, and in an organization’s code standards».[9]
History[edit]
The first hardware exception handling was found in the UNIVAC I from 1951. Arithmetic overflow executed two instructions at address 0, which could transfer control or fix up the result.[10]
Software exception handling developed in the 1960s and 1970s. LISP 1.5 (1958-1961)[11] allowed exceptions to be raised by the ERROR pseudo-function, similarly to errors raised by the interpreter or compiler. Exceptions were caught by the ERRORSET keyword, which returned NIL in case of an error, instead of terminating the program or entering the debugger.[12]
PL/I introduced its own form of exception handling circa 1964, allowing interrupts to be handled with ON units.[13]
MacLisp observed that ERRSET and ERR were used not only for error raising, but for non-local control flow, and thus added two new keywords, CATCH and THROW (June 1972).[14] The cleanup behavior now generally called «finally» was introduced in NIL (New Implementation of LISP) in the mid- to late-1970s as UNWIND-PROTECT.[15] This was then adopted by Common Lisp. Contemporary with this was dynamic-wind in Scheme, which handled exceptions in closures. The first papers on structured exception handling were Goodenough (1975a) and Goodenough (1975b).[16] Exception handling was subsequently widely adopted by many programming languages from the 1980s onward.
Hardware exceptions[edit]
There is no clear consensus as to the exact meaning of an exception with respect to hardware.[17] From the implementation point of view, it is handled identically to an interrupt: the processor halts execution of the current program, looks up the interrupt handler in the interrupt vector table for that exception or interrupt condition, saves state, and switches control.
IEEE 754 floating-point exceptions[edit]
Exception handling in the IEEE 754 floating-point standard refers in general to exceptional conditions and defines an exception as «an event that occurs when an operation on some particular operands has no outcome suitable for every reasonable application. That operation might signal one or more exceptions by invoking the default or, if explicitly requested, a language-defined alternate handling.»
By default, an IEEE 754 exception is resumable and is handled by substituting a predefined value for different exceptions, e.g. infinity for a divide by zero exception, and providing status flags for later checking of whether the exception occurred (see C99 programming language for a typical example of handling of IEEE 754 exceptions). An exception-handling style enabled by the use of status flags involves: first computing an expression using a fast, direct implementation; checking whether it failed by testing status flags; and then, if necessary, calling a slower, more numerically robust, implementation.[18]
The IEEE 754 standard uses the term «trapping» to refer to the calling of a user-supplied exception-handling routine on exceptional conditions, and is an optional feature of the standard. The standard recommends several usage scenarios for this, including the implementation of non-default pre-substitution of a value followed by resumption, to concisely handle removable singularities.[18][19][20]
The default IEEE 754 exception handling behaviour of resumption following pre-substitution of a default value avoids the risks inherent in changing flow of program control on numerical exceptions. For example, the 1996 Cluster spacecraft launch ended in a catastrophic explosion due in part to the Ada exception handling policy of aborting computation on arithmetic error. William Kahan claims the default IEEE 754 exception handling behavior would have prevented this.[19]
Exception support in programming languages[edit]
Software exception handling and the support provided by software tools differs somewhat from what is understood by exception handling in hardware, but similar concepts are involved. In programming language mechanisms for exception handling, the term exception is typically used in a specific sense to denote a data structure storing information about an exceptional condition. One mechanism to transfer control, or raise an exception, is known as a throw. The exception is said to be thrown. Execution is transferred to a «catch».
Programming languages differ substantially in their notion of what an exception is. Contemporary languages can roughly be divided into two groups:[9]
- Languages where exceptions are designed to be used as flow control structures: Ada, Modula-3, ML, OCaml, PL/I, Python, and Ruby fall in this category. For example, Python’s iterators throw StopIteration exceptions to signal that there are no further items produced by the iterator.[21]
- Languages where exceptions are only used to handle abnormal, unpredictable, erroneous situations: C++,[22] Java,[23] C#, Common Lisp, Eiffel, and Modula-2.
PL/I used dynamically scoped exceptions. PL/I exception handling included events that are not errors, e.g., attention, end-of-file, modification of listed variables.[citation needed]
Syntax[edit]
Many computer languages have built-in syntactic support for exceptions and exception handling. This includes ActionScript, Ada, BlitzMax, C++, C#, Clojure, COBOL, D, ECMAScript, Eiffel, Java, ML, Object Pascal (e.g. Delphi, Free Pascal, and the like), PowerBuilder, Objective-C, OCaml, PHP (as of version 5), PL/I, PL/SQL, Prolog, Python, REALbasic, Ruby, Scala, Seed7, Smalltalk, Tcl, Visual Prolog and most .NET languages.
Excluding minor syntactic differences, there are only a couple of exception handling styles in use. In the most popular style, an exception is initiated by a special statement (throw or raise) with an exception object (e.g. with Java or Object Pascal) or a value of a special extendable enumerated type (e.g. with Ada or SML). The scope for exception handlers starts with a marker clause (try or the language’s block starter such as begin) and ends in the start of the first handler clause (catch, except, rescue). Several handler clauses can follow, and each can specify which exception types it handles and what name it uses for the exception object. As a minor variation, some languages use a single handler clause, which deals with the class of the exception internally.
Also common is a related clause (finally or ensure) that is executed whether an exception occurred or not, typically to release resources acquired within the body of the exception-handling block. Notably, C++ does not provide this construct, recommending instead the Resource Acquisition Is Initialization (RAII) technique which frees resources using destructors.[24] According to a 2008 paper by Westley Weimer and George Necula, the syntax of the try…finally blocks in Java is a contributing factor to software defects. When a method needs to handle the acquisition and release of 3–5 resources, programmers are apparently unwilling to nest enough blocks due to readability concerns, even when this would be a correct solution. It is possible to use a single try…finally block even when dealing with multiple resources, but that requires a correct use of sentinel values, which is another common source of bugs for this type of problem.[25]: 8:6–8:7
Python and Ruby also permit a clause (else) that is used in case no exception occurred before the end of the handler’s scope was reached.
In its whole, exception handling code might look like this (in Java-like pseudocode):
try { line = console.readLine(); if (line.length() == 0) { throw new EmptyLineException("The line read from console was empty!"); } console.printLine("Hello %s!" % line); } catch (EmptyLineException e) { console.printLine("Hello!"); } catch (Exception e) { console.printLine("Error: " + e.message()); } else { console.printLine("The program ran successfully."); } finally { console.printLine("The program is now terminating."); }
C does not have try-catch exception handling, but uses return codes for error checking. The setjmp and longjmp standard library functions can be used to implement try-catch handling via macros.[26]
Perl 5 uses die for throw and eval {} if ($@) {} for try-catch. It has CPAN modules that offer try-catch semantics.[27]
Termination and resumption semantics[edit]
When an exception is thrown, the program searches back through the stack of function calls until an exception handler is found. Some languages call for unwinding the stack as this search progresses. That is, if function f, containing a handler H for exception E, calls function g, which in turn calls function h, and an exception E occurs in h, then functions h and g may be terminated, and H in f will handle E. This is said to be termination semantics.
Alternately, the exception handling mechanisms may not unwind the stack on entry[note 1] to an exception handler, giving the exception handler the option to restart the computation, resume or unwind. This allows the program to continue the computation at exactly the same place where the error occurred (for example when a previously missing file has become available) or to implement notifications, logging, queries and fluid variables on top of the exception handling mechanism (as done in Smalltalk). Allowing the computation to resume where it left off is termed resumption semantics.
There are theoretical and design arguments in favor of either decision. C++ standardization discussions in 1989–1991 resulted in a definitive decision to use termination semantics in C++.[28] Bjarne Stroustrup cites a presentation by Jim Mitchell as a key data point:
Jim had used exception handling in half a dozen languages over a period of 20 years and was an early proponent of resumption semantics as one of the main designers and implementers of Xerox’s Cedar/Mesa system. His message was
- “termination is preferred over resumption; this is not a matter of opinion but a matter of years of experience. Resumption is seductive, but not valid.”
He backed this statement with experience from several operating systems. The key example was Cedar/Mesa: It was written by people who liked and used resumption, but after ten years of use, there was only one use of resumption left in the half million line system – and that was a context inquiry. Because resumption wasn’t actually necessary for such a context inquiry, they removed it and found a significant speed increase in that part of the system. In each and every case where resumption had been used it had – over the ten years – become a problem and a more appropriate design had replaced it. Basically, every use of resumption had represented a failure to keep separate levels of abstraction disjoint.[16]
Exception-handling languages with resumption include Common Lisp with its Condition System, PL/I, Dylan, R,[29] and Smalltalk. However, the majority of newer programming languages follow C++ and use termination semantics.
Exception handling implementation[edit]
The implementation of exception handling in programming languages typically involves a fair amount of support from both a code generator and the runtime system accompanying a compiler. (It was the addition of exception handling to C++ that ended the useful lifetime of the original C++ compiler, Cfront.[30]) Two schemes are most common. The first, dynamic registration, generates code that continually updates structures about the program state in terms of exception handling.[31] Typically, this adds a new element to the stack frame layout that knows what handlers are available for the function or method associated with that frame; if an exception is thrown, a pointer in the layout directs the runtime to the appropriate handler code. This approach is compact in terms of space, but adds execution overhead on frame entry and exit. It was commonly used in many Ada implementations, for example, where complex generation and runtime support was already needed for many other language features. Microsoft’s 32-bit Structured Exception Handling (SEH) uses this approach with a separate exception stack.[32] Dynamic registration, being fairly straightforward to define, is amenable to proof of correctness.[33]
The second scheme, and the one implemented in many production-quality C++ compilers and 64-bit Microsoft SEH, is a table-driven approach. This creates static tables at compile time and link time that relate ranges of the program counter to the program state with respect to exception handling.[34] Then, if an exception is thrown, the runtime system looks up the current instruction location in the tables and determines what handlers are in play and what needs to be done. This approach minimizes executive overhead for the case where an exception is not thrown. This happens at the cost of some space, but this space can be allocated into read-only, special-purpose data sections that are not loaded or relocated until an exception is actually thrown.[35] The location (in memory) of the code for handling an exception need not be located within (or even near) the region of memory where the rest of the function’s code is stored. So if an exception is thrown then a performance hit – roughly comparable to a function call[36] – may occur if the necessary exception handling code needs to be loaded/cached. However, this scheme has minimal performance cost if no exception is thrown. Since exceptions in C++ are supposed to be exceptional (i.e. uncommon/rare) events, the phrase «zero-cost exceptions»[note 2] is sometimes used to describe exception handling in C++. Like runtime type identification (RTTI), exceptions might not adhere to C++’s zero-overhead principle as implementing exception handling at run-time requires a non-zero amount of memory for the lookup table.[37] For this reason, exception handling (and RTTI) can be disabled in many C++ compilers, which may be useful for systems with very limited memory[37] (such as embedded systems). This second approach is also superior in terms of achieving thread safety[citation needed].
Other definitional and implementation schemes have been proposed as well. For languages that support metaprogramming, approaches that involve no overhead at all (beyond the already present support for reflection) have been advanced.[38]
Exception handling based on design by contract[edit]
A different view of exceptions is based on the principles of design by contract and is supported in particular by the Eiffel language. The idea is to provide a more rigorous basis for exception handling by defining precisely what is «normal» and «abnormal» behavior. Specifically, the approach is based on two concepts:
- Failure: the inability of an operation to fulfill its contract. For example, an addition may produce an arithmetic overflow (it does not fulfill its contract of computing a good approximation to the mathematical sum); or a routine may fail to meet its postcondition.
- Exception: an abnormal event occurring during the execution of a routine (that routine is the «recipient» of the exception) during its execution. Such an abnormal event results from the failure of an operation called by the routine.
The «Safe Exception Handling principle» as introduced by Bertrand Meyer in Object-Oriented Software Construction then holds that there are only two meaningful ways a routine can react when an exception occurs:
- Failure, or «organized panic»: The routine fixes the object’s state by re-establishing the invariant (this is the «organized» part), and then fails (panics), triggering an exception in its caller (so that the abnormal event is not ignored).
- Retry: The routine tries the algorithm again, usually after changing some values so that the next attempt will have a better chance to succeed.
In particular, simply ignoring an exception is not permitted; a block must either be retried and successfully complete, or propagate the exception to its caller.
Here is an example expressed in Eiffel syntax. It assumes that a routine send_fast is normally the better way to send a message, but it may fail, triggering an exception; if so, the algorithm next uses send_slow, which will fail less often. If send_slow fails, the routine send as a whole should fail, causing the caller to get an exception.
send (m: MESSAGE) is -- Send m through fast link, if possible, otherwise through slow link. local tried_fast, tried_slow: BOOLEAN do if tried_fast then tried_slow := True send_slow (m) else tried_fast := True send_fast (m) end rescue if not tried_slow then retry end end
The boolean local variables are initialized to False at the start. If send_fast fails, the body (do clause) will be executed again, causing execution of send_slow. If this execution of send_slow fails, the rescue clause will execute to the end with no retry (no else clause in the final if), causing the routine execution as a whole to fail.
This approach has the merit of defining clearly what «normal» and «abnormal» cases are: an abnormal case, causing an exception, is one in which the routine is unable to fulfill its contract. It defines a clear distribution of roles: the do clause (normal body) is in charge of achieving, or attempting to achieve, the routine’s contract; the rescue clause is in charge of reestablishing the context and restarting the process, if this has a chance of succeeding, but not of performing any actual computation.
Although exceptions in Eiffel have a fairly clear philosophy, Kiniry (2006) criticizes their implementation because «Exceptions that are part of the language definition are represented by INTEGER values, developer-defined exceptions by STRING values. […] Additionally, because they are basic values and not objects, they have no inherent semantics beyond that which is expressed in a helper routine which necessarily cannot be foolproof because of the representation overloading in effect (e.g., one cannot
differentiate two integers of the same value).»[9]
Uncaught exceptions[edit]
Contemporary applications face many design challenges when considering exception handling strategies. Particularly in modern enterprise level applications, exceptions must often cross process boundaries and machine boundaries. Part of designing a solid exception handling strategy is recognizing when a process has failed to the point where it cannot be economically handled by the software portion of the process.[39]
If an exception is thrown and not caught (operationally, an exception is thrown when there is no applicable handler specified), the uncaught exception is handled by the runtime; the routine that does this is called the uncaught exception handler.[40][41] The most common default behavior is to terminate the program and print an error message to the console, usually including debug information such as a string representation of the exception and the stack trace.[40][42][43] This is often avoided by having a top-level (application-level) handler (for example in an event loop) that catches exceptions before they reach the runtime.[40][44]
Note that even though an uncaught exception may result in the program terminating abnormally (the program may not be correct if an exception is not caught, notably by not rolling back partially completed transactions, or not releasing resources), the process terminates normally (assuming the runtime works correctly), as the runtime (which is controlling execution of the program) can ensure orderly shutdown of the process.
In a multithreaded program, an uncaught exception in a thread may instead result in termination of just that thread, not the entire process (uncaught exceptions in the thread-level handler are caught by the top-level handler). This is particularly important for servers, where for example a servlet (running in its own thread) can be terminated without the server overall being affected.
This default uncaught exception handler may be overridden, either globally or per-thread, for example to provide alternative logging or end-user reporting of uncaught exceptions, or to restart threads that terminate due to an uncaught exception. For example, in Java this is done for a single thread via Thread.setUncaughtExceptionHandler and globally via Thread.setDefaultUncaughtExceptionHandler; in Python this is done by modifying sys.excepthook.
Checked exceptions[edit]
Java introduced the notion of checked exceptions,[45][46] which are special classes of exceptions. The checked exceptions that a method may raise must be part of the method’s signature. For instance, if a method might throw an IOException, it must declare this fact explicitly in its method signature. Failure to do so raises a compile-time error. According to Hanspeter Mössenböck, checked exceptions are less convenient but more robust.[47] Checked exceptions can, at compile time, reduce the incidence of unhandled exceptions surfacing at runtime in a given application.
Kiniry writes that «As any Java programmer knows, the volume of try catch code in a typical Java application is sometimes larger than the comparable code necessary for explicit formal parameter and return value checking in other languages that do not have checked exceptions. In fact, the general consensus among in-the-trenches Java programmers is that dealing with checked exceptions is nearly as unpleasant a task as writing documentation. Thus, many programmers report that they “resent” checked exceptions.».[9] Martin Fowler has written «…on the whole I think that exceptions are good, but Java checked exceptions are more trouble than they are worth.»[48] As of 2006 no major programming language has followed Java in adding checked exceptions.[48] For example, C# does not require or allow declaration of any exception specifications, with the following posted by Eric Gunnerson:[49][9][48]
«Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality.»
Anders Hejlsberg describes two concerns with checked exceptions:[50]
- Versioning: A method may be declared to throw exceptions X and Y. In a later version of the code, one cannot throw exception Z from the method, because it would make the new code incompatible with the earlier uses. Checked exceptions require the method’s callers to either add Z to their throws clause or handle the exception. Alternately, Z may be misrepresented as an X or a Y.
- Scalability: In a hierarchical design, each systems may have several subsystems. Each subsystem may throw several exceptions. Each parent system must deal with the exceptions of all subsystems below it, resulting in an exponential number of exceptions to be dealt with. Checked exceptions require all of these exceptions to be dealt with explicitly.
To work around these, Hejlsberg says programmers resort to circumventing the feature by using a throws Exception declaration. Another circumvention is to use a try { ... } catch (Exception e) {} handler.[50] This is referred to as catch-all exception handling or Pokémon exception handling after the show’s catchphrase «Gotta Catch ‘Em All!».[51] The Java Tutorials discourage catch-all exception handling as it may catch exceptions «for which the handler was not intended».[52] Still another discouraged circumvention is to make all exceptions subclass RuntimeException.[53] An encouraged solution is to use a catch-all handler or throws clause but with a specific superclass of all potentially thrown exceptions rather than the general superclass Exception. Another encouraged solution is to define and declare exception types that are suitable for the level of abstraction of the called method[54] and map lower level exceptions to these types by using exception chaining.
Similar mechanisms[edit]
The roots of checked exceptions go back to the CLU programming language’s notion of exception specification.[55] A function could raise only exceptions listed in its type, but any leaking exceptions from called functions would automatically be turned into the sole runtime exception, failure, instead of resulting in compile-time error.[7] Later, Modula-3 had a similar feature.[56] These features don’t include the compile time checking that is central in the concept of checked exceptions.[55]
Early versions of the C++ programming language included an optional mechanism similar to checked exceptions, called exception specifications. By default any function could throw any exception, but this could be limited by a throw clause added to the function signature, that specified which exceptions the function may throw. Exception specifications were not enforced at compile-time. Violations resulted in the global function std::unexpected being called.[57] An empty exception specification could be given, which indicated that the function will throw no exception. This was not made the default when exception handling was added to the language because it would have required too much modification of existing code, would have impeded interaction with code written in other languages, and would have tempted programmers into writing too many handlers at the local level.[57] Explicit use of empty exception specifications could, however, allow C++ compilers to perform significant code and stack layout optimizations that are precluded when exception handling may take place in a function.[35] Some analysts viewed the proper use of exception specifications in C++ as difficult to achieve.[58] This use of exception specifications was included in C++98 and C++03, deprecated in the 2012 C++ language standard (C++11),[59] and was removed from the language in C++17. A function that will not throw any exceptions can now be denoted by the noexcept keyword.
An uncaught exceptions analyzer exists for the OCaml programming language.[60] The tool reports the set of raised exceptions as an extended type signature. But, unlike checked exceptions, the tool does not require any syntactic annotations and is external (i.e. it is possible to compile and run a program without having checked the exceptions).
Dynamic checking of exceptions[edit]
The point of exception handling routines is to ensure that the code can handle error conditions. In order to establish that exception handling routines are sufficiently robust, it is necessary to present the code with a wide spectrum of invalid or unexpected inputs, such as can be created via software fault injection and mutation testing (that is also sometimes referred to as fuzz testing). One of the most difficult types of software for which to write exception handling routines is protocol software, since a robust protocol implementation must be prepared to receive input that does not comply with the relevant specification(s).
In order to ensure that meaningful regression analysis can be conducted throughout a software development lifecycle process, any exception handling testing should be highly automated, and the test cases must be generated in a scientific, repeatable fashion. Several commercially available systems exist that perform such testing.
In runtime engine environments such as Java or .NET, there exist tools that attach to the runtime engine and every time that an exception of interest occurs, they record debugging information that existed in memory at the time the exception was thrown (call stack and heap values). These tools are called automated exception handling or error interception tools and provide ‘root-cause’ information for exceptions.
Asynchronous exceptions[edit]
Asynchronous exceptions are events raised by a separate thread or external process, such as pressing Ctrl-C to interrupt a program, receiving a signal, or sending a disruptive message such as «stop» or «suspend» from another thread of execution.[61][62] Whereas synchronous exceptions happen at a specific throw statement, asynchronous exceptions can be raised at any time. It follows that asynchronous exception handling can’t be optimized out by the compiler, as it cannot prove the absence of asynchronous exceptions. They are also difficult to program with correctly, as asynchronous exceptions must be blocked during cleanup operations to avoid resource leaks.
Programming languages typically avoid or restrict asynchronous exception handling, for example C++ forbids raising exceptions from signal handlers, and Java has deprecated the use of its ThreadDeath exception that was used to allow one thread to stop another one.[63] Another feature is a semi-asynchronous mechanism that raises an asynchronous exception only during certain operations of the program. For example Java’s Thread.interrupt() only affects the thread when the thread calls an operation that throws InterruptedException.[64] The similar POSIX pthread_cancel API has race conditions which make it impossible to use safely.[65]
Condition systems[edit]
Common Lisp, Dylan and Smalltalk have a condition system[66] (see Common Lisp Condition System) that encompasses the aforementioned exception handling systems. In those languages or environments the advent of a condition (a «generalisation of an error» according to Kent Pitman) implies a function call, and only late in the exception handler the decision to unwind the stack may be taken.
Conditions are a generalization of exceptions. When a condition arises, an appropriate condition handler is searched for and selected, in stack order, to handle the condition. Conditions that do not represent errors may safely go unhandled entirely; their only purpose may be to propagate hints or warnings toward the user.[67]
Continuable exceptions[edit]
This is related to the so-called resumption model of exception handling, in which some exceptions are said to be continuable: it is permitted to return to the expression that signaled an exception, after having taken corrective action in the handler. The condition system is generalized thus: within the handler of a non-serious condition (a.k.a. continuable exception), it is possible to jump to predefined restart points (a.k.a. restarts) that lie between the signaling expression and the condition handler. Restarts are functions closed over some lexical environment, allowing the programmer to repair this environment before exiting the condition handler completely or unwinding the stack even partially.
An example is the ENDPAGE condition in PL/I; the ON unit might write page trailer lines and header lines for the next page, then fall through to resume execution of the interrupted code.
Restarts separate mechanism from policy[edit]
Condition handling moreover provides a separation of mechanism from policy. Restarts provide various possible mechanisms for recovering from error, but do not select which mechanism is appropriate in a given situation. That is the province of the condition handler, which (since it is located in higher-level code) has access to a broader view.
An example: Suppose there is a library function whose purpose is to parse a single syslog file entry. What should this function do if the entry is malformed? There is no one right answer, because the same library could be deployed in programs for many different purposes. In an interactive log-file browser, the right thing to do might be to return the entry unparsed, so the user can see it—but in an automated log-summarizing program, the right thing to do might be to supply null values for the unreadable fields, but abort with an error, if too many entries have been malformed.
That is to say, the question can only be answered in terms of the broader goals of the program, which are not known to the general-purpose library function. Nonetheless, exiting with an error message is only rarely the right answer. So instead of simply exiting with an error, the function may establish restarts offering various ways to continue—for instance, to skip the log entry, to supply default or null values for the unreadable fields, to ask the user for the missing values, or to unwind the stack and abort processing with an error message. The restarts offered constitute the mechanisms available for recovering from error; the selection of restart by the condition handler supplies the policy.
Criticism[edit]
Exception handling is often not handled correctly in software, especially when there are multiple sources of exceptions; data flow analysis of 5 million lines of Java code found over 1300 exception handling defects.[25]
Citing multiple prior studies by others (1999–2004) and their own results, Weimer and Necula wrote that a significant problem with exceptions is that they «create hidden control-flow paths that are difficult for programmers to reason about».[25]: 8:27 «While try-catch-finally is conceptually simple, it has the most complicated execution description in the language specification [Gosling et al. 1996] and requires four levels of nested “if”s in its official English description. In short, it contains a large number of corner cases that programmers often overlook.»[25]: 8:13–8:14
Exceptions, as unstructured flow, increase the risk of resource leaks (such as escaping a section locked by a mutex, or one temporarily holding a file open) or inconsistent state. There are various techniques for resource management in the presence of exceptions, most commonly combining the dispose pattern with some form of unwind protection (like a finally clause), which automatically releases the resource when control exits a section of code.
Tony Hoare in 1980 described the Ada programming language as having «…a plethora of features and notational conventions, many of them unnecessary and some of them, like exception handling, even dangerous. […] Do not allow this language in its present state to be used in applications where reliability is critical […]. The next rocket to go astray as a result of a programming language error may not be an exploratory space rocket on a harmless trip to Venus: It may be a nuclear warhead exploding over one of our own cities.»[68]
The Go developers believe that the try-catch-finally idiom obfuscates control flow,[69] and introduced the exception-like panic/recover mechanism.[70] recover() differs from catch in that it can only be called from within a defer code block in a function, so the handler can only do clean-up and change the function’s return values, and cannot return control to an arbitrary point within the function.[71] The defer block itself functions similarly to a finally clause.
Exception handling in UI hierarchies[edit]
Front-end web frameworks, such as React and Vue, have introduced error handling mechanisms where errors propagate up the UI component hierarchy, in a way that is analogous to how errors propagate up the call stack in executing code.[72][73] Here the error boundary mechanism serves as an analogue to the typical try-catch mechanism. Thus a component can ensure that errors from its child components are caught and handled, and not propagated up to parent components.
For example, in Vue, a component would catch errors by implementing errorCaptured
Vue.component('parent', { template: '<div><slot></slot></div>', errorCaptured: (err, vm, info) => alert('An error occurred'); }) Vue.component('child', { template: '<div>{{ cause_error() }}</div>' })
When used like this in markup:
<parent> <child></child> </parent>
The error produced by the child component is caught and handled by the parent component.[74]
See also[edit]
- Automated exception handling
- Exception safety
- Continuation
- Defensive programming
- Triple fault
- Option types and Result types, alternative ways of handling errors in functional programming without exceptions
- Data validation
Notes[edit]
- ^ In, e.g., PL/I, a normal exit from an exception handler unwinds the stack.
- ^ There is «zero [processing] cost» only if no exception is throw (although there will be a memory cost since memory is needed for the lookup table). There is a (potentially significant) cost if an exception is thrown (that is, if
throwis executed). Implementing exception handling may also limit the possible compiler optimizations that may be performed.
References[edit]
- ^ a b Cristian, Flaviu (1980). «Exception Handling and Software Fault Tolerance». Proc. 10th Int. Symp. On Fault Tolerant Computing (FTCS-25 reprint ed.) (6): 531–540. CiteSeerX 10.1.1.116.8736. doi:10.1109/TC.1982.1676035. OCLC 1029229019. S2CID 18345469.
- ^ Goodenough 1975b, pp. 683–684.
- ^ Goodenough 1975b, p. 684.
- ^ Black 1982, pp. 13–15.
- ^ a b Lang, Jun; Stewart, David B. (March 1998). «A study of the applicability of existing exception-handling techniques to component-based real-time software technology». ACM Transactions on Programming Languages and Systems. 20 (2): 276. CiteSeerX 10.1.1.33.3400. doi:10.1145/276393.276395. S2CID 18875882.
Perhaps the most common form of exception-handling method used by software programmers is the «return-code» technique that was popularized as part of C and UNIX.
- ^ Levin 1977, p. 5.
- ^ a b Liskov, B.H.; Snyder, A. (November 1979). «Exception Handling in CLU» (PDF). IEEE Transactions on Software Engineering. SE-5 (6): 546–558. doi:10.1109/TSE.1979.230191. S2CID 15506879. Retrieved 19 December 2021.
- ^ Levin 1977, p. 4.
- ^ a b c d e Kiniry, J. R. (2006). «Exceptions in Java and Eiffel: Two Extremes in Exception Design and Application». Advanced Topics in Exception Handling Techniques (PDF). Lecture Notes in Computer Science. Vol. 4119. pp. 288–300. doi:10.1007/11818502_16. ISBN 978-3-540-37443-5.
- ^ Smotherman, Mark. «Interrupts». Retrieved 4 January 2022.
- ^ McCarthy, John (12 February 1979). «History of Lisp». www-formal.stanford.edu. Retrieved 13 January 2022.
- ^ McCarthy, John; Levin, Michael I.; Abrahams, Paul W.; Edwards, Daniel J.; Hart, Timothy P. (14 July 1961). LISP 1.5 programmer’s manual (PDF). Retrieved 13 January 2022.
- ^ «The ON Statement» (PDF). IBM System/360 Operating System, PL/I Language Specifications (PDF). IBM. July 1966. p. 120. C28-6571-3.
- ^ Gabriel & Steele 2008, p. 3.
- ^ White 1979, p. 194.
- ^ a b Stroustrup 1994, p. 392.
- ^ Hyde, Randall. «Art of Assembly: Chapter Seventeen». www.plantation-productions.com. Retrieved 22 December 2021.
- ^ a b Xiaoye Li; James Demmel (1994). «Faster Numerical Algorithms via Exception Handling, IEEE Transactions on Computers, 43(8)»: 983–992.
- ^ a b W.Kahan (July 5, 2005). «A Demonstration of Presubstitution for ∞/∞» (PDF). Archived (PDF) from the original on March 10, 2012.
- ^ Hauser, John R. (March 1996). «Handling floating-point exceptions in numeric programs». ACM Transactions on Programming Languages and Systems. 18 (2): 139–174. doi:10.1145/227699.227701. S2CID 9820157.
- ^ «Built-in Exceptions — Python 3.10.4 documentation». docs.python.org. Retrieved 17 May 2022.
- ^ «Stroustrup: C++ Style and Technique FAQ». www.stroustrup.com. Archived from the original on 2 February 2018. Retrieved 5 May 2018.
- ^
Bloch, Joshua (2008). «Item 57: Use exceptions only for exceptional situations». Effective Java (Second ed.). Addison-Wesley. p. 241. ISBN 978-0-321-35668-0. - ^ Stroustrup, Bjarne. «C++ Style and Technique FAQ». www.stroustrup.com. Retrieved 12 January 2022.
- ^ a b c d Weimer, W; Necula, G.C. (2008). «Exceptional Situations and Program Reliability» (PDF). ACM Transactions on Programming Languages and Systems. Vol. 30, no. 2. Archived (PDF) from the original on 2015-09-23.
- ^ Roberts, Eric S. (21 March 1989). «Implementing Exceptions in C» (PDF). DEC Systems Research Center. SRC-RR-40. Retrieved 4 January 2022.
- ^ Christiansen, Tom; Torkington, Nathan (2003). «10.12. Handling Exceptions». Perl cookbook (2nd ed.). Beijing: O’Reilly. ISBN 0-596-00313-7.
- ^ Stroustrup 1994, 16.6 Exception Handling: Resumption vs. Termination, pp. 390–393.
- ^ «R: Condition Handling and Recovery». search.r-project.org. Retrieved 2022-12-05.
- ^ Scott Meyers, The Most Important C++ Software…Ever Archived 2011-04-28 at the Wayback Machine, 2006
- ^ D. Cameron, P. Faust, D. Lenkov, M. Mehta, «A portable implementation of C++ exception handling», Proceedings of the C++ Conference (August 1992) USENIX.
- ^ Peter Kleissner (February 14, 2009). «Windows Exception Handling — Peter Kleissner». Archived from the original on October 14, 2013. Retrieved 2009-11-21., Compiler based Structured Exception Handling section
- ^ Graham Hutton, Joel Wright, «Compiling Exceptions Correctly Archived 2014-09-11 at the Wayback Machine». Proceedings of the 7th International Conference on Mathematics of Program Construction, 2004.
- ^ Lajoie, Josée (March–April 1994). «Exception handling – Supporting the runtime mechanism». C++ Report. 6 (3).
- ^ a b Schilling, Jonathan L. (August 1998). «Optimizing away C++ exception handling». SIGPLAN Notices. 33 (8): 40–47. doi:10.1145/286385.286390. S2CID 1522664.
- ^ «Modern C++ best practices for exceptions and error handling». Microsoft. 8 March 2021. Retrieved 21 March 2022.
- ^ a b Stroustrup, Bjarne (18 November 2019). «C++ exceptions and alternatives» (PDF). Retrieved 23 March 2022.
- ^ M. Hof, H. Mössenböck, P. Pirkelbauer, «Zero-Overhead Exception Handling Using Metaprogramming Archived 2016-03-03 at the Wayback Machine», Proceedings SOFSEM’97, November 1997, Lecture Notes in Computer Science 1338, pp. 423-431.
- ^ All Exceptions Are Handled, Jim Wilcox, «All Exceptions Are Handled». Archived from the original on 2015-03-18. Retrieved 2014-12-08.
- ^ a b c Mac Developer Library, «Uncaught Exceptions Archived 2016-03-04 at the Wayback Machine»
- ^ MSDN, AppDomain.UnhandledException Event Archived 2016-03-04 at the Wayback Machine
- ^ The Python Tutorial, «8. Errors and Exceptions Archived 2015-09-01 at the Wayback Machine»
- ^ «Java Practices -> Provide an uncaught exception handler». www.javapractices.com. Archived from the original on 9 September 2016. Retrieved 5 May 2018.
- ^ PyMOTW (Python Module Of The Week), «Exception Handling Archived 2015-09-15 at the Wayback Machine»
- ^ «Google Answers: The origin of checked exceptions». Archived from the original on 2011-08-06. Retrieved 2011-12-15.
- ^ Java Language Specification, chapter 11.2. http://java.sun.com/docs/books/jls/third_edition/html/exceptions.html#11.2 Archived 2006-12-08 at the Wayback Machine
- ^ Mössenböck, Hanspeter (2002-03-25). «Advanced C#: Variable Number of Parameters» (PDF). Institut für Systemsoftware, Johannes Kepler Universität Linz, Fachbereich Informatik. p. 32. Archived (PDF) from the original on 2011-09-20. Retrieved 2011-08-05.
- ^ a b c Eckel, Bruce (2006). Thinking in Java (4th ed.). Upper Saddle River, NJ: Prentice Hall. pp. 347–348. ISBN 0-13-187248-6.
- ^ Gunnerson, Eric (9 November 2000). «C# and exception specifications». Archived from the original on 1 January 2006.
- ^ a b Bill Venners; Bruce Eckel (August 18, 2003). «The Trouble with Checked Exceptions: A Conversation with Anders Hejlsberg, Part II». Retrieved 4 January 2022.
- ^ Juneau, Josh (31 May 2017). Java 9 Recipes: A Problem-Solution Approach. Apress. p. 226. ISBN 978-1-4842-1976-8.
- ^ «Advantages of Exceptions (The Java™ Tutorials : Essential Classes : Exceptions)». Download.oracle.com. Archived from the original on 2011-10-26. Retrieved 2011-12-15.
- ^ «Unchecked Exceptions – The Controversy (The Java™ Tutorials : Essential Classes : Exceptions)». Download.oracle.com. Archived from the original on 2011-11-17. Retrieved 2011-12-15.
- ^ Bloch 2001:178 Bloch, Joshua (2001). Effective Java Programming Language Guide. Addison-Wesley Professional. ISBN 978-0-201-31005-4.
- ^ a b «Bruce Eckel’s MindView, Inc: Does Java need Checked Exceptions?». Mindview.net. Archived from the original on 2002-04-05. Retrieved 2011-12-15.
- ^ «Modula-3 — Procedure Types». .cs.columbia.edu. 1995-03-08. Archived from the original on 2008-05-09. Retrieved 2011-12-15.
- ^ a b Bjarne Stroustrup, The C++ Programming Language Third Edition, Addison Wesley, 1997. ISBN 0-201-88954-4. pp. 375-380.
- ^ Reeves, J.W. (July 1996). «Ten Guidelines for Exception Specifications». C++ Report. 8 (7).
- ^ Sutter, Herb (3 March 2010). «Trip Report: March 2010 ISO C++ Standards Meeting». Archived from the original on 23 March 2010. Retrieved 24 March 2010.
- ^ «OcamlExc — An uncaught exceptions analyzer for Objective Caml». Caml.inria.fr. Archived from the original on 2011-08-06. Retrieved 2011-12-15.
- ^ «Asynchronous Exceptions in Haskell — Marlow, Jones, Moran (ResearchIndex)». Citeseer.ist.psu.edu. Archived from the original on 2011-02-23. Retrieved 2011-12-15.
- ^ Freund, Stephen N.; Mitchell, Mark P. «Safe Asynchronous Exceptions For Python» (PDF). Retrieved 4 January 2022.
- ^ «Java Thread Primitive Deprecation». Java.sun.com. Archived from the original on 2009-04-26. Retrieved 2011-12-15.
- ^ «Interrupts (The Java™ Tutorials > Essential Java Classes > Concurrency)». docs.oracle.com. Retrieved 5 January 2022.
- ^ Felker, Rich. «Thread cancellation and resource leaks». ewontfix.com. Retrieved 5 January 2022.
- ^ What Conditions (Exceptions) are Really About (2008-03-24). «What Conditions (Exceptions) are Really About». Danweinreb.org. Archived from the original on February 1, 2013. Retrieved 2014-09-18.
- ^ «Condition System Concepts». Franz.com. 2009-07-21. Archived from the original on 2007-06-28. Retrieved 2011-12-15.
- ^ C.A.R. Hoare. «The Emperor’s Old Clothes». 1980 Turing Award Lecture
- ^ «Frequently Asked Questions». Archived from the original on 2017-05-03. Retrieved 2017-04-27.
We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.
- ^ Panic And Recover Archived 2013-10-24 at the Wayback Machine, Go wiki
- ^ Bendersky, Eli (8 August 2018). «On the uses and misuses of panics in Go». Eli Bendersky’s website. Retrieved 5 January 2022.
The specific limitation is that recover can only be called in a defer code block, which cannot return control to an arbitrary point, but can only do clean-ups and tweak the function’s return values.
- ^ «Error Boundaries». React. Retrieved 2018-12-10.
- ^ «Vue.js API». Vue.js. Retrieved 2018-12-10.
- ^ «Error handling with Vue.js». CatchJS. Retrieved 2018-12-10.
- Black, Andrew P. (January 1982). Exception handling: The case against (PDF) (PhD). University of Oxford. CiteSeerX 10.1.1.94.5554. OCLC 123311492.
- Gabriel, Richard P.; Steele, Guy L. (2008). A Pattern of Language Evolution (PDF). LISP50: Celebrating the 50th Anniversary of Lisp. pp. 1–10. doi:10.1145/1529966.1529967. ISBN 978-1-60558-383-9.
- Goodenough, John B. (1975a). Structured exception handling. Proceedings of the 2nd ACM SIGACT-SIGPLAN symposium on Principles of programming languages — POPL ’75. pp. 204–224. doi:10.1145/512976.512997.
- Goodenough, John B. (1975). «Exception handling: Issues and a proposed notation» (PDF). Communications of the ACM. 18 (12): 683–696. CiteSeerX 10.1.1.122.7791. doi:10.1145/361227.361230. S2CID 12935051.
- Levin, Roy (June 1977). Program Structures for Exceptional Condition Handling (PDF) (PhD). Carnegie-Mellon University. DTIC ADA043449. Archived (PDF) from the original on December 22, 2021.
- Stroustrup, Bjarne (1994). The design and evolution of C++ (1st ed.). Reading, Mass.: Addison-Wesley. ISBN 0-201-54330-3.
- White, Jon L (May 1979). NIL — A Perspective (PDF). Proceedings of the 1979 Macsyma User’s Conference.
External links[edit]
- A Crash Course on the Depths of Win32 Structured Exception Handling by Matt Pietrek — Microsoft Systems Journal (1997)
- Article «C++ Exception Handling» by Christophe de Dinechin
- Article «Exceptional practices» by Brian Goetz
- Article «Object Oriented Exception Handling in Perl» by Arun Udaya Shankar
- Article «Programming with Exceptions in C++» by Kyle Loudon
- Article «Unchecked Exceptions — The Controversy»
- Conference slides Floating-Point Exception-Handling policies (pdf p. 46) by William Kahan
- Descriptions from Portland Pattern Repository
- Does Java Need Checked Exceptions?
In computing and computer programming, exception handling is the process of responding to the occurrence of exceptions – anomalous or exceptional conditions requiring special processing – during the execution of a program. In general, an exception breaks the normal flow of execution and executes a pre-registered exception handler; the details of how this is done depend on whether it is a hardware or software exception and how the software exception is implemented. Exception handling, if provided, is facilitated by specialized programming language constructs, hardware mechanisms like interrupts, or operating system (OS) inter-process communication (IPC) facilities like signals. Some exceptions, especially hardware ones, may be handled so gracefully that execution can resume where it was interrupted.
Definition[edit]
The definition of an exception is based on the observation that each procedure has a precondition, a set of circumstances for which it will terminate «normally».[1] An exception handling mechanism allows the procedure to raise an exception[2] if this precondition is violated,[1] for example if the procedure has been called on an abnormal set of arguments. The exception handling mechanism then handles the exception.[3]
The precondition, and the definition of exception, is subjective. The set of «normal» circumstances is defined entirely by the programmer, e.g. the programmer may deem division by zero to be undefined, hence an exception, or devise some behavior such as returning zero or a special «ZERO DIVIDE» value (circumventing the need for exceptions).[4] Common exceptions include an invalid argument (e.g. value is outside of the domain of a function), an unavailable resource (like a missing file, a hard disk error, or out-of-memory errors), or that the routine has detected a normal condition that requires special handling, e.g., attention, end of file.
Exception handling solves the semipredicate problem, in that the mechanism distinguishes normal return values from erroneous ones. In languages without built-in exception handling such as C, routines would need to signal the error in some other way, such as the common return code and errno pattern.[5] Taking a broad view, errors can be considered to be a proper subset of exceptions,[6] and explicit error mechanisms such as errno can be considered (verbose) forms of exception handling.[5] The term «exception» is preferred to «error» because it does not imply that anything is wrong — a condition viewed as an error by one procedure or programmer may not be viewed that way by another. Even the term «exception» may be misleading because its typical connotation of «outlier» indicates that something infrequent or unusual has occurred, when in fact raising the exception may be a normal and usual situation in the program.[7] For example, suppose a lookup function for an associative array throws an exception if the key has no value associated. Depending on context, this «key absent» exception may occur much more often than a successful lookup.[8]
A major influence on the scope and use of exceptions is social pressure, i.e. «examples of use, typically found in core libraries, and code examples in technical books, magazine articles, and online discussion forums, and in an organization’s code standards».[9]
History[edit]
The first hardware exception handling was found in the UNIVAC I from 1951. Arithmetic overflow executed two instructions at address 0, which could transfer control or fix up the result.[10]
Software exception handling developed in the 1960s and 1970s. LISP 1.5 (1958-1961)[11] allowed exceptions to be raised by the ERROR pseudo-function, similarly to errors raised by the interpreter or compiler. Exceptions were caught by the ERRORSET keyword, which returned NIL in case of an error, instead of terminating the program or entering the debugger.[12]
PL/I introduced its own form of exception handling circa 1964, allowing interrupts to be handled with ON units.[13]
MacLisp observed that ERRSET and ERR were used not only for error raising, but for non-local control flow, and thus added two new keywords, CATCH and THROW (June 1972).[14] The cleanup behavior now generally called «finally» was introduced in NIL (New Implementation of LISP) in the mid- to late-1970s as UNWIND-PROTECT.[15] This was then adopted by Common Lisp. Contemporary with this was dynamic-wind in Scheme, which handled exceptions in closures. The first papers on structured exception handling were Goodenough (1975a) and Goodenough (1975b).[16] Exception handling was subsequently widely adopted by many programming languages from the 1980s onward.
Hardware exceptions[edit]
There is no clear consensus as to the exact meaning of an exception with respect to hardware.[17] From the implementation point of view, it is handled identically to an interrupt: the processor halts execution of the current program, looks up the interrupt handler in the interrupt vector table for that exception or interrupt condition, saves state, and switches control.
IEEE 754 floating-point exceptions[edit]
Exception handling in the IEEE 754 floating-point standard refers in general to exceptional conditions and defines an exception as «an event that occurs when an operation on some particular operands has no outcome suitable for every reasonable application. That operation might signal one or more exceptions by invoking the default or, if explicitly requested, a language-defined alternate handling.»
By default, an IEEE 754 exception is resumable and is handled by substituting a predefined value for different exceptions, e.g. infinity for a divide by zero exception, and providing status flags for later checking of whether the exception occurred (see C99 programming language for a typical example of handling of IEEE 754 exceptions). An exception-handling style enabled by the use of status flags involves: first computing an expression using a fast, direct implementation; checking whether it failed by testing status flags; and then, if necessary, calling a slower, more numerically robust, implementation.[18]
The IEEE 754 standard uses the term «trapping» to refer to the calling of a user-supplied exception-handling routine on exceptional conditions, and is an optional feature of the standard. The standard recommends several usage scenarios for this, including the implementation of non-default pre-substitution of a value followed by resumption, to concisely handle removable singularities.[18][19][20]
The default IEEE 754 exception handling behaviour of resumption following pre-substitution of a default value avoids the risks inherent in changing flow of program control on numerical exceptions. For example, the 1996 Cluster spacecraft launch ended in a catastrophic explosion due in part to the Ada exception handling policy of aborting computation on arithmetic error. William Kahan claims the default IEEE 754 exception handling behavior would have prevented this.[19]
Exception support in programming languages[edit]
Software exception handling and the support provided by software tools differs somewhat from what is understood by exception handling in hardware, but similar concepts are involved. In programming language mechanisms for exception handling, the term exception is typically used in a specific sense to denote a data structure storing information about an exceptional condition. One mechanism to transfer control, or raise an exception, is known as a throw. The exception is said to be thrown. Execution is transferred to a «catch».
Programming languages differ substantially in their notion of what an exception is. Contemporary languages can roughly be divided into two groups:[9]
- Languages where exceptions are designed to be used as flow control structures: Ada, Modula-3, ML, OCaml, PL/I, Python, and Ruby fall in this category. For example, Python’s iterators throw StopIteration exceptions to signal that there are no further items produced by the iterator.[21]
- Languages where exceptions are only used to handle abnormal, unpredictable, erroneous situations: C++,[22] Java,[23] C#, Common Lisp, Eiffel, and Modula-2.
PL/I used dynamically scoped exceptions. PL/I exception handling included events that are not errors, e.g., attention, end-of-file, modification of listed variables.[citation needed]
Syntax[edit]
Many computer languages have built-in syntactic support for exceptions and exception handling. This includes ActionScript, Ada, BlitzMax, C++, C#, Clojure, COBOL, D, ECMAScript, Eiffel, Java, ML, Object Pascal (e.g. Delphi, Free Pascal, and the like), PowerBuilder, Objective-C, OCaml, PHP (as of version 5), PL/I, PL/SQL, Prolog, Python, REALbasic, Ruby, Scala, Seed7, Smalltalk, Tcl, Visual Prolog and most .NET languages.
Excluding minor syntactic differences, there are only a couple of exception handling styles in use. In the most popular style, an exception is initiated by a special statement (throw or raise) with an exception object (e.g. with Java or Object Pascal) or a value of a special extendable enumerated type (e.g. with Ada or SML). The scope for exception handlers starts with a marker clause (try or the language’s block starter such as begin) and ends in the start of the first handler clause (catch, except, rescue). Several handler clauses can follow, and each can specify which exception types it handles and what name it uses for the exception object. As a minor variation, some languages use a single handler clause, which deals with the class of the exception internally.
Also common is a related clause (finally or ensure) that is executed whether an exception occurred or not, typically to release resources acquired within the body of the exception-handling block. Notably, C++ does not provide this construct, recommending instead the Resource Acquisition Is Initialization (RAII) technique which frees resources using destructors.[24] According to a 2008 paper by Westley Weimer and George Necula, the syntax of the try…finally blocks in Java is a contributing factor to software defects. When a method needs to handle the acquisition and release of 3–5 resources, programmers are apparently unwilling to nest enough blocks due to readability concerns, even when this would be a correct solution. It is possible to use a single try…finally block even when dealing with multiple resources, but that requires a correct use of sentinel values, which is another common source of bugs for this type of problem.[25]: 8:6–8:7
Python and Ruby also permit a clause (else) that is used in case no exception occurred before the end of the handler’s scope was reached.
In its whole, exception handling code might look like this (in Java-like pseudocode):
try { line = console.readLine(); if (line.length() == 0) { throw new EmptyLineException("The line read from console was empty!"); } console.printLine("Hello %s!" % line); } catch (EmptyLineException e) { console.printLine("Hello!"); } catch (Exception e) { console.printLine("Error: " + e.message()); } else { console.printLine("The program ran successfully."); } finally { console.printLine("The program is now terminating."); }
C does not have try-catch exception handling, but uses return codes for error checking. The setjmp and longjmp standard library functions can be used to implement try-catch handling via macros.[26]
Perl 5 uses die for throw and eval {} if ($@) {} for try-catch. It has CPAN modules that offer try-catch semantics.[27]
Termination and resumption semantics[edit]
When an exception is thrown, the program searches back through the stack of function calls until an exception handler is found. Some languages call for unwinding the stack as this search progresses. That is, if function f, containing a handler H for exception E, calls function g, which in turn calls function h, and an exception E occurs in h, then functions h and g may be terminated, and H in f will handle E. This is said to be termination semantics.
Alternately, the exception handling mechanisms may not unwind the stack on entry[note 1] to an exception handler, giving the exception handler the option to restart the computation, resume or unwind. This allows the program to continue the computation at exactly the same place where the error occurred (for example when a previously missing file has become available) or to implement notifications, logging, queries and fluid variables on top of the exception handling mechanism (as done in Smalltalk). Allowing the computation to resume where it left off is termed resumption semantics.
There are theoretical and design arguments in favor of either decision. C++ standardization discussions in 1989–1991 resulted in a definitive decision to use termination semantics in C++.[28] Bjarne Stroustrup cites a presentation by Jim Mitchell as a key data point:
Jim had used exception handling in half a dozen languages over a period of 20 years and was an early proponent of resumption semantics as one of the main designers and implementers of Xerox’s Cedar/Mesa system. His message was
- “termination is preferred over resumption; this is not a matter of opinion but a matter of years of experience. Resumption is seductive, but not valid.”
He backed this statement with experience from several operating systems. The key example was Cedar/Mesa: It was written by people who liked and used resumption, but after ten years of use, there was only one use of resumption left in the half million line system – and that was a context inquiry. Because resumption wasn’t actually necessary for such a context inquiry, they removed it and found a significant speed increase in that part of the system. In each and every case where resumption had been used it had – over the ten years – become a problem and a more appropriate design had replaced it. Basically, every use of resumption had represented a failure to keep separate levels of abstraction disjoint.[16]
Exception-handling languages with resumption include Common Lisp with its Condition System, PL/I, Dylan, R,[29] and Smalltalk. However, the majority of newer programming languages follow C++ and use termination semantics.
Exception handling implementation[edit]
The implementation of exception handling in programming languages typically involves a fair amount of support from both a code generator and the runtime system accompanying a compiler. (It was the addition of exception handling to C++ that ended the useful lifetime of the original C++ compiler, Cfront.[30]) Two schemes are most common. The first, dynamic registration, generates code that continually updates structures about the program state in terms of exception handling.[31] Typically, this adds a new element to the stack frame layout that knows what handlers are available for the function or method associated with that frame; if an exception is thrown, a pointer in the layout directs the runtime to the appropriate handler code. This approach is compact in terms of space, but adds execution overhead on frame entry and exit. It was commonly used in many Ada implementations, for example, where complex generation and runtime support was already needed for many other language features. Microsoft’s 32-bit Structured Exception Handling (SEH) uses this approach with a separate exception stack.[32] Dynamic registration, being fairly straightforward to define, is amenable to proof of correctness.[33]
The second scheme, and the one implemented in many production-quality C++ compilers and 64-bit Microsoft SEH, is a table-driven approach. This creates static tables at compile time and link time that relate ranges of the program counter to the program state with respect to exception handling.[34] Then, if an exception is thrown, the runtime system looks up the current instruction location in the tables and determines what handlers are in play and what needs to be done. This approach minimizes executive overhead for the case where an exception is not thrown. This happens at the cost of some space, but this space can be allocated into read-only, special-purpose data sections that are not loaded or relocated until an exception is actually thrown.[35] The location (in memory) of the code for handling an exception need not be located within (or even near) the region of memory where the rest of the function’s code is stored. So if an exception is thrown then a performance hit – roughly comparable to a function call[36] – may occur if the necessary exception handling code needs to be loaded/cached. However, this scheme has minimal performance cost if no exception is thrown. Since exceptions in C++ are supposed to be exceptional (i.e. uncommon/rare) events, the phrase «zero-cost exceptions»[note 2] is sometimes used to describe exception handling in C++. Like runtime type identification (RTTI), exceptions might not adhere to C++’s zero-overhead principle as implementing exception handling at run-time requires a non-zero amount of memory for the lookup table.[37] For this reason, exception handling (and RTTI) can be disabled in many C++ compilers, which may be useful for systems with very limited memory[37] (such as embedded systems). This second approach is also superior in terms of achieving thread safety[citation needed].
Other definitional and implementation schemes have been proposed as well. For languages that support metaprogramming, approaches that involve no overhead at all (beyond the already present support for reflection) have been advanced.[38]
Exception handling based on design by contract[edit]
A different view of exceptions is based on the principles of design by contract and is supported in particular by the Eiffel language. The idea is to provide a more rigorous basis for exception handling by defining precisely what is «normal» and «abnormal» behavior. Specifically, the approach is based on two concepts:
- Failure: the inability of an operation to fulfill its contract. For example, an addition may produce an arithmetic overflow (it does not fulfill its contract of computing a good approximation to the mathematical sum); or a routine may fail to meet its postcondition.
- Exception: an abnormal event occurring during the execution of a routine (that routine is the «recipient» of the exception) during its execution. Such an abnormal event results from the failure of an operation called by the routine.
The «Safe Exception Handling principle» as introduced by Bertrand Meyer in Object-Oriented Software Construction then holds that there are only two meaningful ways a routine can react when an exception occurs:
- Failure, or «organized panic»: The routine fixes the object’s state by re-establishing the invariant (this is the «organized» part), and then fails (panics), triggering an exception in its caller (so that the abnormal event is not ignored).
- Retry: The routine tries the algorithm again, usually after changing some values so that the next attempt will have a better chance to succeed.
In particular, simply ignoring an exception is not permitted; a block must either be retried and successfully complete, or propagate the exception to its caller.
Here is an example expressed in Eiffel syntax. It assumes that a routine send_fast is normally the better way to send a message, but it may fail, triggering an exception; if so, the algorithm next uses send_slow, which will fail less often. If send_slow fails, the routine send as a whole should fail, causing the caller to get an exception.
send (m: MESSAGE) is -- Send m through fast link, if possible, otherwise through slow link. local tried_fast, tried_slow: BOOLEAN do if tried_fast then tried_slow := True send_slow (m) else tried_fast := True send_fast (m) end rescue if not tried_slow then retry end end
The boolean local variables are initialized to False at the start. If send_fast fails, the body (do clause) will be executed again, causing execution of send_slow. If this execution of send_slow fails, the rescue clause will execute to the end with no retry (no else clause in the final if), causing the routine execution as a whole to fail.
This approach has the merit of defining clearly what «normal» and «abnormal» cases are: an abnormal case, causing an exception, is one in which the routine is unable to fulfill its contract. It defines a clear distribution of roles: the do clause (normal body) is in charge of achieving, or attempting to achieve, the routine’s contract; the rescue clause is in charge of reestablishing the context and restarting the process, if this has a chance of succeeding, but not of performing any actual computation.
Although exceptions in Eiffel have a fairly clear philosophy, Kiniry (2006) criticizes their implementation because «Exceptions that are part of the language definition are represented by INTEGER values, developer-defined exceptions by STRING values. […] Additionally, because they are basic values and not objects, they have no inherent semantics beyond that which is expressed in a helper routine which necessarily cannot be foolproof because of the representation overloading in effect (e.g., one cannot
differentiate two integers of the same value).»[9]
Uncaught exceptions[edit]
Contemporary applications face many design challenges when considering exception handling strategies. Particularly in modern enterprise level applications, exceptions must often cross process boundaries and machine boundaries. Part of designing a solid exception handling strategy is recognizing when a process has failed to the point where it cannot be economically handled by the software portion of the process.[39]
If an exception is thrown and not caught (operationally, an exception is thrown when there is no applicable handler specified), the uncaught exception is handled by the runtime; the routine that does this is called the uncaught exception handler.[40][41] The most common default behavior is to terminate the program and print an error message to the console, usually including debug information such as a string representation of the exception and the stack trace.[40][42][43] This is often avoided by having a top-level (application-level) handler (for example in an event loop) that catches exceptions before they reach the runtime.[40][44]
Note that even though an uncaught exception may result in the program terminating abnormally (the program may not be correct if an exception is not caught, notably by not rolling back partially completed transactions, or not releasing resources), the process terminates normally (assuming the runtime works correctly), as the runtime (which is controlling execution of the program) can ensure orderly shutdown of the process.
In a multithreaded program, an uncaught exception in a thread may instead result in termination of just that thread, not the entire process (uncaught exceptions in the thread-level handler are caught by the top-level handler). This is particularly important for servers, where for example a servlet (running in its own thread) can be terminated without the server overall being affected.
This default uncaught exception handler may be overridden, either globally or per-thread, for example to provide alternative logging or end-user reporting of uncaught exceptions, or to restart threads that terminate due to an uncaught exception. For example, in Java this is done for a single thread via Thread.setUncaughtExceptionHandler and globally via Thread.setDefaultUncaughtExceptionHandler; in Python this is done by modifying sys.excepthook.
Checked exceptions[edit]
Java introduced the notion of checked exceptions,[45][46] which are special classes of exceptions. The checked exceptions that a method may raise must be part of the method’s signature. For instance, if a method might throw an IOException, it must declare this fact explicitly in its method signature. Failure to do so raises a compile-time error. According to Hanspeter Mössenböck, checked exceptions are less convenient but more robust.[47] Checked exceptions can, at compile time, reduce the incidence of unhandled exceptions surfacing at runtime in a given application.
Kiniry writes that «As any Java programmer knows, the volume of try catch code in a typical Java application is sometimes larger than the comparable code necessary for explicit formal parameter and return value checking in other languages that do not have checked exceptions. In fact, the general consensus among in-the-trenches Java programmers is that dealing with checked exceptions is nearly as unpleasant a task as writing documentation. Thus, many programmers report that they “resent” checked exceptions.».[9] Martin Fowler has written «…on the whole I think that exceptions are good, but Java checked exceptions are more trouble than they are worth.»[48] As of 2006 no major programming language has followed Java in adding checked exceptions.[48] For example, C# does not require or allow declaration of any exception specifications, with the following posted by Eric Gunnerson:[49][9][48]
«Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality.»
Anders Hejlsberg describes two concerns with checked exceptions:[50]
- Versioning: A method may be declared to throw exceptions X and Y. In a later version of the code, one cannot throw exception Z from the method, because it would make the new code incompatible with the earlier uses. Checked exceptions require the method’s callers to either add Z to their throws clause or handle the exception. Alternately, Z may be misrepresented as an X or a Y.
- Scalability: In a hierarchical design, each systems may have several subsystems. Each subsystem may throw several exceptions. Each parent system must deal with the exceptions of all subsystems below it, resulting in an exponential number of exceptions to be dealt with. Checked exceptions require all of these exceptions to be dealt with explicitly.
To work around these, Hejlsberg says programmers resort to circumventing the feature by using a throws Exception declaration. Another circumvention is to use a try { ... } catch (Exception e) {} handler.[50] This is referred to as catch-all exception handling or Pokémon exception handling after the show’s catchphrase «Gotta Catch ‘Em All!».[51] The Java Tutorials discourage catch-all exception handling as it may catch exceptions «for which the handler was not intended».[52] Still another discouraged circumvention is to make all exceptions subclass RuntimeException.[53] An encouraged solution is to use a catch-all handler or throws clause but with a specific superclass of all potentially thrown exceptions rather than the general superclass Exception. Another encouraged solution is to define and declare exception types that are suitable for the level of abstraction of the called method[54] and map lower level exceptions to these types by using exception chaining.
Similar mechanisms[edit]
The roots of checked exceptions go back to the CLU programming language’s notion of exception specification.[55] A function could raise only exceptions listed in its type, but any leaking exceptions from called functions would automatically be turned into the sole runtime exception, failure, instead of resulting in compile-time error.[7] Later, Modula-3 had a similar feature.[56] These features don’t include the compile time checking that is central in the concept of checked exceptions.[55]
Early versions of the C++ programming language included an optional mechanism similar to checked exceptions, called exception specifications. By default any function could throw any exception, but this could be limited by a throw clause added to the function signature, that specified which exceptions the function may throw. Exception specifications were not enforced at compile-time. Violations resulted in the global function std::unexpected being called.[57] An empty exception specification could be given, which indicated that the function will throw no exception. This was not made the default when exception handling was added to the language because it would have required too much modification of existing code, would have impeded interaction with code written in other languages, and would have tempted programmers into writing too many handlers at the local level.[57] Explicit use of empty exception specifications could, however, allow C++ compilers to perform significant code and stack layout optimizations that are precluded when exception handling may take place in a function.[35] Some analysts viewed the proper use of exception specifications in C++ as difficult to achieve.[58] This use of exception specifications was included in C++98 and C++03, deprecated in the 2012 C++ language standard (C++11),[59] and was removed from the language in C++17. A function that will not throw any exceptions can now be denoted by the noexcept keyword.
An uncaught exceptions analyzer exists for the OCaml programming language.[60] The tool reports the set of raised exceptions as an extended type signature. But, unlike checked exceptions, the tool does not require any syntactic annotations and is external (i.e. it is possible to compile and run a program without having checked the exceptions).
Dynamic checking of exceptions[edit]
The point of exception handling routines is to ensure that the code can handle error conditions. In order to establish that exception handling routines are sufficiently robust, it is necessary to present the code with a wide spectrum of invalid or unexpected inputs, such as can be created via software fault injection and mutation testing (that is also sometimes referred to as fuzz testing). One of the most difficult types of software for which to write exception handling routines is protocol software, since a robust protocol implementation must be prepared to receive input that does not comply with the relevant specification(s).
In order to ensure that meaningful regression analysis can be conducted throughout a software development lifecycle process, any exception handling testing should be highly automated, and the test cases must be generated in a scientific, repeatable fashion. Several commercially available systems exist that perform such testing.
In runtime engine environments such as Java or .NET, there exist tools that attach to the runtime engine and every time that an exception of interest occurs, they record debugging information that existed in memory at the time the exception was thrown (call stack and heap values). These tools are called automated exception handling or error interception tools and provide ‘root-cause’ information for exceptions.
Asynchronous exceptions[edit]
Asynchronous exceptions are events raised by a separate thread or external process, such as pressing Ctrl-C to interrupt a program, receiving a signal, or sending a disruptive message such as «stop» or «suspend» from another thread of execution.[61][62] Whereas synchronous exceptions happen at a specific throw statement, asynchronous exceptions can be raised at any time. It follows that asynchronous exception handling can’t be optimized out by the compiler, as it cannot prove the absence of asynchronous exceptions. They are also difficult to program with correctly, as asynchronous exceptions must be blocked during cleanup operations to avoid resource leaks.
Programming languages typically avoid or restrict asynchronous exception handling, for example C++ forbids raising exceptions from signal handlers, and Java has deprecated the use of its ThreadDeath exception that was used to allow one thread to stop another one.[63] Another feature is a semi-asynchronous mechanism that raises an asynchronous exception only during certain operations of the program. For example Java’s Thread.interrupt() only affects the thread when the thread calls an operation that throws InterruptedException.[64] The similar POSIX pthread_cancel API has race conditions which make it impossible to use safely.[65]
Condition systems[edit]
Common Lisp, Dylan and Smalltalk have a condition system[66] (see Common Lisp Condition System) that encompasses the aforementioned exception handling systems. In those languages or environments the advent of a condition (a «generalisation of an error» according to Kent Pitman) implies a function call, and only late in the exception handler the decision to unwind the stack may be taken.
Conditions are a generalization of exceptions. When a condition arises, an appropriate condition handler is searched for and selected, in stack order, to handle the condition. Conditions that do not represent errors may safely go unhandled entirely; their only purpose may be to propagate hints or warnings toward the user.[67]
Continuable exceptions[edit]
This is related to the so-called resumption model of exception handling, in which some exceptions are said to be continuable: it is permitted to return to the expression that signaled an exception, after having taken corrective action in the handler. The condition system is generalized thus: within the handler of a non-serious condition (a.k.a. continuable exception), it is possible to jump to predefined restart points (a.k.a. restarts) that lie between the signaling expression and the condition handler. Restarts are functions closed over some lexical environment, allowing the programmer to repair this environment before exiting the condition handler completely or unwinding the stack even partially.
An example is the ENDPAGE condition in PL/I; the ON unit might write page trailer lines and header lines for the next page, then fall through to resume execution of the interrupted code.
Restarts separate mechanism from policy[edit]
Condition handling moreover provides a separation of mechanism from policy. Restarts provide various possible mechanisms for recovering from error, but do not select which mechanism is appropriate in a given situation. That is the province of the condition handler, which (since it is located in higher-level code) has access to a broader view.
An example: Suppose there is a library function whose purpose is to parse a single syslog file entry. What should this function do if the entry is malformed? There is no one right answer, because the same library could be deployed in programs for many different purposes. In an interactive log-file browser, the right thing to do might be to return the entry unparsed, so the user can see it—but in an automated log-summarizing program, the right thing to do might be to supply null values for the unreadable fields, but abort with an error, if too many entries have been malformed.
That is to say, the question can only be answered in terms of the broader goals of the program, which are not known to the general-purpose library function. Nonetheless, exiting with an error message is only rarely the right answer. So instead of simply exiting with an error, the function may establish restarts offering various ways to continue—for instance, to skip the log entry, to supply default or null values for the unreadable fields, to ask the user for the missing values, or to unwind the stack and abort processing with an error message. The restarts offered constitute the mechanisms available for recovering from error; the selection of restart by the condition handler supplies the policy.
Criticism[edit]
Exception handling is often not handled correctly in software, especially when there are multiple sources of exceptions; data flow analysis of 5 million lines of Java code found over 1300 exception handling defects.[25]
Citing multiple prior studies by others (1999–2004) and their own results, Weimer and Necula wrote that a significant problem with exceptions is that they «create hidden control-flow paths that are difficult for programmers to reason about».[25]: 8:27 «While try-catch-finally is conceptually simple, it has the most complicated execution description in the language specification [Gosling et al. 1996] and requires four levels of nested “if”s in its official English description. In short, it contains a large number of corner cases that programmers often overlook.»[25]: 8:13–8:14
Exceptions, as unstructured flow, increase the risk of resource leaks (such as escaping a section locked by a mutex, or one temporarily holding a file open) or inconsistent state. There are various techniques for resource management in the presence of exceptions, most commonly combining the dispose pattern with some form of unwind protection (like a finally clause), which automatically releases the resource when control exits a section of code.
Tony Hoare in 1980 described the Ada programming language as having «…a plethora of features and notational conventions, many of them unnecessary and some of them, like exception handling, even dangerous. […] Do not allow this language in its present state to be used in applications where reliability is critical […]. The next rocket to go astray as a result of a programming language error may not be an exploratory space rocket on a harmless trip to Venus: It may be a nuclear warhead exploding over one of our own cities.»[68]
The Go developers believe that the try-catch-finally idiom obfuscates control flow,[69] and introduced the exception-like panic/recover mechanism.[70] recover() differs from catch in that it can only be called from within a defer code block in a function, so the handler can only do clean-up and change the function’s return values, and cannot return control to an arbitrary point within the function.[71] The defer block itself functions similarly to a finally clause.
Exception handling in UI hierarchies[edit]
Front-end web frameworks, such as React and Vue, have introduced error handling mechanisms where errors propagate up the UI component hierarchy, in a way that is analogous to how errors propagate up the call stack in executing code.[72][73] Here the error boundary mechanism serves as an analogue to the typical try-catch mechanism. Thus a component can ensure that errors from its child components are caught and handled, and not propagated up to parent components.
For example, in Vue, a component would catch errors by implementing errorCaptured
Vue.component('parent', { template: '<div><slot></slot></div>', errorCaptured: (err, vm, info) => alert('An error occurred'); }) Vue.component('child', { template: '<div>{{ cause_error() }}</div>' })
When used like this in markup:
<parent> <child></child> </parent>
The error produced by the child component is caught and handled by the parent component.[74]
See also[edit]
- Automated exception handling
- Exception safety
- Continuation
- Defensive programming
- Triple fault
- Option types and Result types, alternative ways of handling errors in functional programming without exceptions
- Data validation
Notes[edit]
- ^ In, e.g., PL/I, a normal exit from an exception handler unwinds the stack.
- ^ There is «zero [processing] cost» only if no exception is throw (although there will be a memory cost since memory is needed for the lookup table). There is a (potentially significant) cost if an exception is thrown (that is, if
throwis executed). Implementing exception handling may also limit the possible compiler optimizations that may be performed.
References[edit]
- ^ a b Cristian, Flaviu (1980). «Exception Handling and Software Fault Tolerance». Proc. 10th Int. Symp. On Fault Tolerant Computing (FTCS-25 reprint ed.) (6): 531–540. CiteSeerX 10.1.1.116.8736. doi:10.1109/TC.1982.1676035. OCLC 1029229019. S2CID 18345469.
- ^ Goodenough 1975b, pp. 683–684.
- ^ Goodenough 1975b, p. 684.
- ^ Black 1982, pp. 13–15.
- ^ a b Lang, Jun; Stewart, David B. (March 1998). «A study of the applicability of existing exception-handling techniques to component-based real-time software technology». ACM Transactions on Programming Languages and Systems. 20 (2): 276. CiteSeerX 10.1.1.33.3400. doi:10.1145/276393.276395. S2CID 18875882.
Perhaps the most common form of exception-handling method used by software programmers is the «return-code» technique that was popularized as part of C and UNIX.
- ^ Levin 1977, p. 5.
- ^ a b Liskov, B.H.; Snyder, A. (November 1979). «Exception Handling in CLU» (PDF). IEEE Transactions on Software Engineering. SE-5 (6): 546–558. doi:10.1109/TSE.1979.230191. S2CID 15506879. Retrieved 19 December 2021.
- ^ Levin 1977, p. 4.
- ^ a b c d e Kiniry, J. R. (2006). «Exceptions in Java and Eiffel: Two Extremes in Exception Design and Application». Advanced Topics in Exception Handling Techniques (PDF). Lecture Notes in Computer Science. Vol. 4119. pp. 288–300. doi:10.1007/11818502_16. ISBN 978-3-540-37443-5.
- ^ Smotherman, Mark. «Interrupts». Retrieved 4 January 2022.
- ^ McCarthy, John (12 February 1979). «History of Lisp». www-formal.stanford.edu. Retrieved 13 January 2022.
- ^ McCarthy, John; Levin, Michael I.; Abrahams, Paul W.; Edwards, Daniel J.; Hart, Timothy P. (14 July 1961). LISP 1.5 programmer’s manual (PDF). Retrieved 13 January 2022.
- ^ «The ON Statement» (PDF). IBM System/360 Operating System, PL/I Language Specifications (PDF). IBM. July 1966. p. 120. C28-6571-3.
- ^ Gabriel & Steele 2008, p. 3.
- ^ White 1979, p. 194.
- ^ a b Stroustrup 1994, p. 392.
- ^ Hyde, Randall. «Art of Assembly: Chapter Seventeen». www.plantation-productions.com. Retrieved 22 December 2021.
- ^ a b Xiaoye Li; James Demmel (1994). «Faster Numerical Algorithms via Exception Handling, IEEE Transactions on Computers, 43(8)»: 983–992.
- ^ a b W.Kahan (July 5, 2005). «A Demonstration of Presubstitution for ∞/∞» (PDF). Archived (PDF) from the original on March 10, 2012.
- ^ Hauser, John R. (March 1996). «Handling floating-point exceptions in numeric programs». ACM Transactions on Programming Languages and Systems. 18 (2): 139–174. doi:10.1145/227699.227701. S2CID 9820157.
- ^ «Built-in Exceptions — Python 3.10.4 documentation». docs.python.org. Retrieved 17 May 2022.
- ^ «Stroustrup: C++ Style and Technique FAQ». www.stroustrup.com. Archived from the original on 2 February 2018. Retrieved 5 May 2018.
- ^
Bloch, Joshua (2008). «Item 57: Use exceptions only for exceptional situations». Effective Java (Second ed.). Addison-Wesley. p. 241. ISBN 978-0-321-35668-0. - ^ Stroustrup, Bjarne. «C++ Style and Technique FAQ». www.stroustrup.com. Retrieved 12 January 2022.
- ^ a b c d Weimer, W; Necula, G.C. (2008). «Exceptional Situations and Program Reliability» (PDF). ACM Transactions on Programming Languages and Systems. Vol. 30, no. 2. Archived (PDF) from the original on 2015-09-23.
- ^ Roberts, Eric S. (21 March 1989). «Implementing Exceptions in C» (PDF). DEC Systems Research Center. SRC-RR-40. Retrieved 4 January 2022.
- ^ Christiansen, Tom; Torkington, Nathan (2003). «10.12. Handling Exceptions». Perl cookbook (2nd ed.). Beijing: O’Reilly. ISBN 0-596-00313-7.
- ^ Stroustrup 1994, 16.6 Exception Handling: Resumption vs. Termination, pp. 390–393.
- ^ «R: Condition Handling and Recovery». search.r-project.org. Retrieved 2022-12-05.
- ^ Scott Meyers, The Most Important C++ Software…Ever Archived 2011-04-28 at the Wayback Machine, 2006
- ^ D. Cameron, P. Faust, D. Lenkov, M. Mehta, «A portable implementation of C++ exception handling», Proceedings of the C++ Conference (August 1992) USENIX.
- ^ Peter Kleissner (February 14, 2009). «Windows Exception Handling — Peter Kleissner». Archived from the original on October 14, 2013. Retrieved 2009-11-21., Compiler based Structured Exception Handling section
- ^ Graham Hutton, Joel Wright, «Compiling Exceptions Correctly Archived 2014-09-11 at the Wayback Machine». Proceedings of the 7th International Conference on Mathematics of Program Construction, 2004.
- ^ Lajoie, Josée (March–April 1994). «Exception handling – Supporting the runtime mechanism». C++ Report. 6 (3).
- ^ a b Schilling, Jonathan L. (August 1998). «Optimizing away C++ exception handling». SIGPLAN Notices. 33 (8): 40–47. doi:10.1145/286385.286390. S2CID 1522664.
- ^ «Modern C++ best practices for exceptions and error handling». Microsoft. 8 March 2021. Retrieved 21 March 2022.
- ^ a b Stroustrup, Bjarne (18 November 2019). «C++ exceptions and alternatives» (PDF). Retrieved 23 March 2022.
- ^ M. Hof, H. Mössenböck, P. Pirkelbauer, «Zero-Overhead Exception Handling Using Metaprogramming Archived 2016-03-03 at the Wayback Machine», Proceedings SOFSEM’97, November 1997, Lecture Notes in Computer Science 1338, pp. 423-431.
- ^ All Exceptions Are Handled, Jim Wilcox, «All Exceptions Are Handled». Archived from the original on 2015-03-18. Retrieved 2014-12-08.
- ^ a b c Mac Developer Library, «Uncaught Exceptions Archived 2016-03-04 at the Wayback Machine»
- ^ MSDN, AppDomain.UnhandledException Event Archived 2016-03-04 at the Wayback Machine
- ^ The Python Tutorial, «8. Errors and Exceptions Archived 2015-09-01 at the Wayback Machine»
- ^ «Java Practices -> Provide an uncaught exception handler». www.javapractices.com. Archived from the original on 9 September 2016. Retrieved 5 May 2018.
- ^ PyMOTW (Python Module Of The Week), «Exception Handling Archived 2015-09-15 at the Wayback Machine»
- ^ «Google Answers: The origin of checked exceptions». Archived from the original on 2011-08-06. Retrieved 2011-12-15.
- ^ Java Language Specification, chapter 11.2. http://java.sun.com/docs/books/jls/third_edition/html/exceptions.html#11.2 Archived 2006-12-08 at the Wayback Machine
- ^ Mössenböck, Hanspeter (2002-03-25). «Advanced C#: Variable Number of Parameters» (PDF). Institut für Systemsoftware, Johannes Kepler Universität Linz, Fachbereich Informatik. p. 32. Archived (PDF) from the original on 2011-09-20. Retrieved 2011-08-05.
- ^ a b c Eckel, Bruce (2006). Thinking in Java (4th ed.). Upper Saddle River, NJ: Prentice Hall. pp. 347–348. ISBN 0-13-187248-6.
- ^ Gunnerson, Eric (9 November 2000). «C# and exception specifications». Archived from the original on 1 January 2006.
- ^ a b Bill Venners; Bruce Eckel (August 18, 2003). «The Trouble with Checked Exceptions: A Conversation with Anders Hejlsberg, Part II». Retrieved 4 January 2022.
- ^ Juneau, Josh (31 May 2017). Java 9 Recipes: A Problem-Solution Approach. Apress. p. 226. ISBN 978-1-4842-1976-8.
- ^ «Advantages of Exceptions (The Java™ Tutorials : Essential Classes : Exceptions)». Download.oracle.com. Archived from the original on 2011-10-26. Retrieved 2011-12-15.
- ^ «Unchecked Exceptions – The Controversy (The Java™ Tutorials : Essential Classes : Exceptions)». Download.oracle.com. Archived from the original on 2011-11-17. Retrieved 2011-12-15.
- ^ Bloch 2001:178 Bloch, Joshua (2001). Effective Java Programming Language Guide. Addison-Wesley Professional. ISBN 978-0-201-31005-4.
- ^ a b «Bruce Eckel’s MindView, Inc: Does Java need Checked Exceptions?». Mindview.net. Archived from the original on 2002-04-05. Retrieved 2011-12-15.
- ^ «Modula-3 — Procedure Types». .cs.columbia.edu. 1995-03-08. Archived from the original on 2008-05-09. Retrieved 2011-12-15.
- ^ a b Bjarne Stroustrup, The C++ Programming Language Third Edition, Addison Wesley, 1997. ISBN 0-201-88954-4. pp. 375-380.
- ^ Reeves, J.W. (July 1996). «Ten Guidelines for Exception Specifications». C++ Report. 8 (7).
- ^ Sutter, Herb (3 March 2010). «Trip Report: March 2010 ISO C++ Standards Meeting». Archived from the original on 23 March 2010. Retrieved 24 March 2010.
- ^ «OcamlExc — An uncaught exceptions analyzer for Objective Caml». Caml.inria.fr. Archived from the original on 2011-08-06. Retrieved 2011-12-15.
- ^ «Asynchronous Exceptions in Haskell — Marlow, Jones, Moran (ResearchIndex)». Citeseer.ist.psu.edu. Archived from the original on 2011-02-23. Retrieved 2011-12-15.
- ^ Freund, Stephen N.; Mitchell, Mark P. «Safe Asynchronous Exceptions For Python» (PDF). Retrieved 4 January 2022.
- ^ «Java Thread Primitive Deprecation». Java.sun.com. Archived from the original on 2009-04-26. Retrieved 2011-12-15.
- ^ «Interrupts (The Java™ Tutorials > Essential Java Classes > Concurrency)». docs.oracle.com. Retrieved 5 January 2022.
- ^ Felker, Rich. «Thread cancellation and resource leaks». ewontfix.com. Retrieved 5 January 2022.
- ^ What Conditions (Exceptions) are Really About (2008-03-24). «What Conditions (Exceptions) are Really About». Danweinreb.org. Archived from the original on February 1, 2013. Retrieved 2014-09-18.
- ^ «Condition System Concepts». Franz.com. 2009-07-21. Archived from the original on 2007-06-28. Retrieved 2011-12-15.
- ^ C.A.R. Hoare. «The Emperor’s Old Clothes». 1980 Turing Award Lecture
- ^ «Frequently Asked Questions». Archived from the original on 2017-05-03. Retrieved 2017-04-27.
We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.
- ^ Panic And Recover Archived 2013-10-24 at the Wayback Machine, Go wiki
- ^ Bendersky, Eli (8 August 2018). «On the uses and misuses of panics in Go». Eli Bendersky’s website. Retrieved 5 January 2022.
The specific limitation is that recover can only be called in a defer code block, which cannot return control to an arbitrary point, but can only do clean-ups and tweak the function’s return values.
- ^ «Error Boundaries». React. Retrieved 2018-12-10.
- ^ «Vue.js API». Vue.js. Retrieved 2018-12-10.
- ^ «Error handling with Vue.js». CatchJS. Retrieved 2018-12-10.
- Black, Andrew P. (January 1982). Exception handling: The case against (PDF) (PhD). University of Oxford. CiteSeerX 10.1.1.94.5554. OCLC 123311492.
- Gabriel, Richard P.; Steele, Guy L. (2008). A Pattern of Language Evolution (PDF). LISP50: Celebrating the 50th Anniversary of Lisp. pp. 1–10. doi:10.1145/1529966.1529967. ISBN 978-1-60558-383-9.
- Goodenough, John B. (1975a). Structured exception handling. Proceedings of the 2nd ACM SIGACT-SIGPLAN symposium on Principles of programming languages — POPL ’75. pp. 204–224. doi:10.1145/512976.512997.
- Goodenough, John B. (1975). «Exception handling: Issues and a proposed notation» (PDF). Communications of the ACM. 18 (12): 683–696. CiteSeerX 10.1.1.122.7791. doi:10.1145/361227.361230. S2CID 12935051.
- Levin, Roy (June 1977). Program Structures for Exceptional Condition Handling (PDF) (PhD). Carnegie-Mellon University. DTIC ADA043449. Archived (PDF) from the original on December 22, 2021.
- Stroustrup, Bjarne (1994). The design and evolution of C++ (1st ed.). Reading, Mass.: Addison-Wesley. ISBN 0-201-54330-3.
- White, Jon L (May 1979). NIL — A Perspective (PDF). Proceedings of the 1979 Macsyma User’s Conference.
External links[edit]
- A Crash Course on the Depths of Win32 Structured Exception Handling by Matt Pietrek — Microsoft Systems Journal (1997)
- Article «C++ Exception Handling» by Christophe de Dinechin
- Article «Exceptional practices» by Brian Goetz
- Article «Object Oriented Exception Handling in Perl» by Arun Udaya Shankar
- Article «Programming with Exceptions in C++» by Kyle Loudon
- Article «Unchecked Exceptions — The Controversy»
- Conference slides Floating-Point Exception-Handling policies (pdf p. 46) by William Kahan
- Descriptions from Portland Pattern Repository
- Does Java Need Checked Exceptions?

Существует две фундаментальные стратегии: обработка исправимых ошибок (исключения, коды возврата по ошибке, функции-обработчики) и неисправимых (assert(), abort()). В каких случаях какую стратегию лучше использовать?
Виды ошибок
Ошибки возникают по разным причинам: пользователь ввёл странные данные, ОС не может дать вам обработчика файла или код разыменовывает (dereferences) nullptr. Каждая из описанных ошибок требует к себе отдельного подхода. По причинам ошибки делятся на три основные категории:
- Пользовательские ошибки: здесь под пользователем подразумевается человек, сидящий перед компьютером и действительно «использующий» программу, а не какой-то программист, дёргающий ваш API. Такие ошибки возникают тогда, когда пользователь делает что-то неправильно.
- Системные ошибки появляются, когда ОС не может выполнить ваш запрос. Иными словами, причина системных ошибок — сбой вызова системного API. Некоторые возникают потому, что программист передал системному вызову плохие параметры, так что это скорее программистская ошибка, а не системная.
- Программистские ошибки случаются, когда программист не учитывает предварительные условия API или языка программирования. Если API требует, чтобы вы не вызывали
foo()с0в качестве первого параметра, а вы это сделали, — виноват программист. Если пользователь ввёл0, который был переданfoo(), а программист не написал проверку вводимых данных, то это опять же его вина.
Каждая из описанных категорий ошибок требует особого подхода к их обработке.
Пользовательские ошибки
Сделаю очень громкое заявление: такие ошибки — на самом деле не ошибки.
Все пользователи не соблюдают инструкции. Программист, имеющий дело с данными, которые вводят люди, должен ожидать, что вводить будут именно плохие данные. Поэтому первым делом нужно проверять их на валидность, сообщать пользователю об обнаруженных ошибках и просить ввести заново.
Поэтому не имеет смысла применять к пользовательским ошибкам какие-либо стратегии обработки. Вводимые данные нужно как можно скорее проверять, чтобы ошибок не возникало.
Конечно, такое не всегда возможно. Иногда проверять вводимые данные слишком дорого, иногда это не позволяет сделать архитектура кода или разделение ответственности. Но в таких случаях ошибки должны обрабатываться однозначно как исправимые. Иначе, допустим, ваша офисная программа будет падать из-за того, что вы нажали backspace в пустом документе, или ваша игра станет вылетать при попытке выстрелить из разряженного оружия.
Если в качестве стратегии обработки исправимых ошибок вы предпочитаете исключения, то будьте осторожны: исключения предназначены только для исключительных ситуаций, к которым не относится большинство случаев ввода пользователями неверных данных. По сути, это даже норма, по мнению многих приложений. Используйте исключения только тогда, когда пользовательские ошибки обнаруживаются в глубине стека вызовов, вероятно, внешнего кода, когда они возникают редко или проявляются очень жёстко. В противном случае лучше сообщать об ошибках с помощью кодов возврата.
Системные ошибки
Обычно системные ошибки нельзя предсказать. Более того, они недетерминистские и могут возникать в программах, которые до этого работали без нареканий. В отличие от пользовательских ошибок, зависящих исключительно от вводимых данных, системные ошибки — настоящие ошибки.
Но как их обрабатывать, как исправимые или неисправимые?
Это зависит от обстоятельств.
Многие считают, что ошибка нехватки памяти — неисправимая. Зачастую не хватает памяти даже для обработки этой ошибки! И тогда приходится просто сразу же прерывать выполнение.
Но падение программы из-за того, что ОС не может выделить сокет, — это не слишком дружелюбное поведение. Так что лучше бросить исключение и позволить catch аккуратно закрыть программу.
Но бросание исключения — не всегда правильный выбор.
Кто-то даже скажет, что он всегда неправильный.
Если вы хотите повторить операцию после её сбоя, то обёртывание функции в try-catch в цикле — медленное решение. Правильный выбор — возврат кода ошибки и цикличное исполнение, пока не будет возвращено правильное значение.
Если вы создаёте вызов API только для себя, то просто выберите подходящий для своей ситуации путь и следуйте ему. Но если вы пишете библиотеку, то не знаете, чего хотят пользователи. Дальше мы разберём подходящую стратегию для этого случая. Для потенциально неисправимых ошибок подойдёт «обработчик ошибок», а при других ошибках необходимо предоставить два варианта развития событий.
Обратите внимание, что не следует использовать подтверждения (assertions), включающиеся только в режиме отладки. Ведь системные ошибки могут возникать и в релизной сборке!
Программистские ошибки
Это худший вид ошибок. Для их обработки я стараюсь сделать так, чтобы мои ошибки были связаны только с вызовами функций, то есть с плохими параметрами. Прочие типы программистских ошибок могут быть пойманы только в runtime, с помощью отладочных макросов (assertion macros), раскиданных по коду.
При работе с плохими параметрами есть две стратегии: дать им определённое или неопределённое поведение.
Если исходное требование для функции — запрет на передачу ей плохих параметров, то, если их передать, это считается неопределённым поведением и должно проверяться не самой функцией, а оператором вызова (caller). Функция должна делать только отладочное подтверждение (debug assertion).
С другой стороны, если отсутствие плохих параметров не является частью исходных требований, а документация определяет, что функция будет бросать bad_parameter_exception при передаче ей плохого параметра, то передача — это хорошо определённое поведение (бросание исключения или любая другая стратегия обработки исправимых ошибок), и функция всегда должна это проверять.
В качестве примера рассмотрим получающие функции (accessor functions) : в спецификации на std::vector<T>operator[] говорится, что индекс должен быть в пределах валидного диапазона, при этом at() сообщает нам, что функция кинет исключение, если индекс не попадает в диапазон. Более того, большинство реализаций стандартных библиотек обеспечивают режим отладки, в котором проверяется индекс operator[], но технически это неопределённое поведение, оно не обязано проверяться.
Примечание: необязательно бросать исключение, чтобы получилось определённое поведение. Пока это не упомянуто в исходных условиях для функции, это считается определённым. Всё, что прописано в исходных условиях, не должно проверяться функцией, это неопределённое поведение.
Когда нужно проверять только с помощью отладочных подтверждений, а когда — постоянно?
К сожалению, однозначного рецепта нет, решение зависит от конкретной ситуации. У меня есть лишь одно проверенное правило, которому я следую при разработке API. Оно основано на наблюдении, что проверять исходные условия должен вызывающий, а не вызываемый. А значит, условие должно быть «проверяемым» для вызывающего. Также условие «проверяемое», если можно легко выполнить операцию, при которой значение параметра всегда будет правильным. Если для параметра это возможно, то это получается исходное условие, а значит, проверяется только посредством отладочного подтверждения (а если слишком дорого, то вообще не проверяется).
Но конечное решение зависит от многих других факторов, так что очень трудно дать какой-то общий совет. По умолчанию я стараюсь свести к неопределённому поведению и использованию только подтверждений. Иногда бывает целесообразно обеспечить оба варианта, как это делает стандартная библиотека с operator[] и at().
Хотя в ряде случаев это может быть ошибкой.
Об иерархии std::exception
Если в качестве стратегии обработки исправимых ошибок вы выбрали исключения, то рекомендуется создать новый класс и наследовать его от одного из классов исключений стандартной библиотеки.
Я предлагаю наследовать только от одного из этих четырёх классов:
std::bad_alloc: для сбоев выделения памяти.std::runtime_error: для общих runtime-ошибок.std::system_error(производное отstd::runtime_error): для системных ошибок с кодами ошибок.std::logic_error: для программистских ошибок с определённым поведением.
Обратите внимание, что в стандартной библиотеке разделяются логические (то есть программистские) и runtime-ошибки. Runtime-ошибки — более широкое определение, чем «системные». Оно описывает «ошибки, обнаруживаемые только при выполнении программы». Такая формулировка не слишком информативна. Лично я использую её для плохих параметров, которые не являются исключительно программистскими ошибками, а могут возникнуть и по вине пользователей. Но это можно определить лишь глубоко в стеке вызовов. Например, плохое форматирование комментариев в standardese приводит к исключению при парсинге, проистекающему из std::runtime_error. Позднее оно ловится на соответствующем уровне и фиксируется в логе. Но я не стал бы использовать этот класс иначе, как и std::logic_error.
Подведём итоги
Есть два пути обработки ошибок:
- как исправимые: используются исключения или возвращаемые значения (в зависимости от ситуации/религии);
- как неисправимые: ошибки журналируются, а программа прерывается.
Подтверждения — это особый вид стратегии обработки неисправимых ошибок, только в режиме отладки.
Есть три основных источника ошибок, каждый требует особого подхода:
- Пользовательские ошибки не должны обрабатываться как ошибки на верхних уровнях программы. Всё, что вводит пользователь, должно проверяться соответствующим образом. Это может обрабатываться как ошибки только на нижних уровнях, которые не взаимодействуют с пользователями напрямую. Применяется стратегия обработки исправимых ошибок.
- Системные ошибки могут обрабатываться в рамках любой из двух стратегий, в зависимости от типа и тяжести. Библиотеки должны работать как можно гибче.
- Программистские ошибки, то есть плохие параметры, могут быть запрещены исходными условиями. В этом случае функция должна использовать только проверку с помощью отладочных подтверждений. Если же речь идёт о полностью определённом поведении, то функции следует предписанным образом сообщать об ошибке. Я стараюсь по умолчанию следовать сценарию с неопределённым поведением и определяю для функции проверку параметров лишь тогда, когда это слишком трудно сделать на стороне вызывающего.
Гибкие методики обработки ошибок в C++
Иногда что-то не работает. Пользователи вводят данные в недопустимом формате, файл не обнаруживается, сетевое соединение сбоит, в системе кончается память. Всё это ошибки, и их надо обрабатывать.
Это относительно легко сделать в высокоуровневых функциях. Вы точно знаете, почему что-то пошло не так, и можете обработать это соответствующим образом. Но в случае с низкоуровневыми функциями всё не так просто. Они не знают, что пошло не так, они знают лишь о самом факте сбоя и должны сообщить об этом тому, кто их вызвал.
В C++ есть два основных подхода: коды возврата ошибок и исключения. Сегодня широко распространено использование исключений. Но некоторые не могут / думают, что не могут / не хотят их использовать — по разным причинам.
Я не буду принимать чью-либо сторону. Вместо этого я опишу методики, которые удовлетворят сторонников обоих подходов. Особенно методики пригодятся разработчикам библиотек.
Проблема
Я работаю над проектом foonathan/memory. Это решение предоставляет различные классы выделения памяти (allocator classes), так что в качестве примера рассмотрим структуру функции выделения.
Для простоты возьмём malloc(). Она возвращает указатель на выделяемую память. Если выделить память не получается, то возвращается nullptr, то есть NULL, то есть ошибочное значение.
У этого решения есть недостатки: вам нужно проверять каждый вызов malloc(). Если вы забудете это сделать, то выделите несуществующую память. Кроме того, по своей натуре коды ошибок транзитивны: если вызвать функцию, которая может вернуть код ошибки, и вы не можете его проигнорировать или обработать, то вы тоже должны вернуть код ошибки.
Это приводит нас к ситуации, когда чередуются нормальные и ошибочные ветви кода. Исключения в таком случае выглядят более подходящим решением. Благодаря им вы сможете обрабатывать ошибки только тогда, когда вам это нужно, а в противном случае — достаточно тихо передать их обратно вызывающему.
Это можно расценить как недостаток.
Но в подобных ситуациях исключения имеют также очень большое преимущество: функция выделения памяти либо возвращает валидную память, либо вообще ничего не возвращает. Это функция «всё или ничего», возвращаемое значение всегда будет валидным. Это полезное следствие согласно принципу Скотта Майера «Make interfaces hard to use incorrectly and easy to use correctly».
Учитывая вышесказанное, можно утверждать, что вам следует использовать исключения в качестве механизма обработки ошибок. Этого мнения придерживается большинство разработчиков на С++, включая и меня. Но проект, которым я занимаюсь, — это библиотека, предоставляющая средства выделения памяти, и предназначена она для приложений, работающих в реальном времени. Для большинства разработчиков подобных приложений (особенно для игроделов) само использование исключений — исключение.
Каламбур детектед.
Чтобы уважить эту группу разработчиков, моей библиотеке лучше обойтись без исключений. Но мне и многим другим они нравятся за элегантность и простоту обработки ошибок, так что ради других разработчиков моей библиотеке лучше использовать исключения.
Так что же делать?
Идеальное решение: возможность включать и отключать исключения по желанию. Но, учитывая природу исключений, нельзя просто менять их местами с кодами ошибок, поскольку у нас не будет внутреннего кода проверки на ошибки — весь внутренний код опирается на предположение о прозрачности исключений. И даже если бы внутри можно было использовать коды ошибок и преобразовывать их в исключения, это лишило бы нас большинства преимуществ последних.
К счастью, я могу определить, что вы делаете, когда обнаруживаете ошибку нехватки памяти: чаще всего вы журналируете это событие и прерываете программу, поскольку она не может корректно работать без памяти. В таких ситуациях исключения — просто способ передачи контроля другой части кода, которая журналирует и прерывает программу. Но есть старый и эффективный способ передачи контроля: указатель функции (function pointer), то есть функция-обработчик (handler function).
Если у вас включены исключения, то вы просто их бросаете. В противном случае вызываете функцию-обработчика и затем прерываете программу. Это предотвратит бесполезную работу функции-обработчика, та позволит программе продолжить выполняться в обычном режиме. Если не прервать, то произойдёт нарушение обязательного постусловия функции: всегда возвращать валидный указатель. Ведь на выполнении этого условия может быть построена работа другого кода, да и вообще это нормальное поведение.
Я называю такой подход обработкой исключений и придерживаюсь его при работе с памятью.
Решение 1: обработчик исключений
Если вам нужно обработать ошибку в условиях, когда наиболее распространённым поведением будет «журналировать и прервать», то можно использовать обработчика исключений. Это такая функция-обработчик, которая вызывается вместо бросания объекта-исключения. Её довольно легко реализовать даже в уже существующем коде. Для этого нужно поместить управление обработкой в класс исключений и обернуть в макрос выражение throw.
Сначала дополним класс и добавим функции для настройки и, возможно, запрашивания функции-обработчика. Я предлагаю делать это так же, как стандартная библиотека обрабатывает std::new_handler:
class my_fatal_error
{
public:
// тип обработчика, он должен брать те же параметры, что и конструктор,
// чтобы у них была одинаковая информация
using handler = void(*)( ... );
// меняет функцию-обработчика
handler set_handler(handler h);
// возвращает текущего обработчика
handler get_handler();
... // нормальное исключение
};
Поскольку это входит в область видимости класса исключений, вам не нужно именовать каким-то особым образом. Отлично, нам же легче.
Если исключения включены, то для удаления обработчика можно использовать условное компилирование (conditional compilation). Если хотите, то также напишите обычный подмешанный класс (mixin class), дающий требуемую функциональность.
Конструктор исключений элегантен: он вызывает текущую функцию-обработчика, передавая ей требуемые аргументы из своих параметров. А затем комбинирует с последующим макросом throw:
If```cpp #if EXCEPTIONS #define THROW(Ex) throw (Ex) #else #define THROW(Ex) (Ex), std::abort() #endif
> Такой макрос throw также предоставляется [foonathan/compatiblity](https://github.com/foonathan/compatibility).
Можно использовать его и так:
```cpp
THROW(my_fatal_error(...))
Если у вас включена поддержка исключений, то будет создан и брошен объект-исключение, всё как обычно. Но если поддержка выключена, то объект-исключение всё равно будет создан, и — это важно — только после этого произойдёт вызов std::abort(). А поскольку конструктор вызывает функцию-обработчика, то он и работает, как требуется: вы получаете точку настройки для журналирования ошибки. Благодаря же вызову std::abort() после конструктора пользователь не может нарушить постусловие.
Когда я работаю с памятью, то при включённых исключениях у меня также включён и обработчик, который вызывается при бросании исключения.
Так что при этой методике вам ещё будет доступна определённая степень кастомизации, даже если вы отключите исключения. Конечно, замена неполноценная, мы только журналируем и прерываем работу программы, без дальнейшего продолжения. Но в ряде случаев, в том числе при исчерпании памяти, это вполне пригодное решение.
А если я хочу продолжить работу после бросания исключения?
Методика с обработчиком исключений не позволяет этого сделать в связи с постусловием кода. Как же тогда продолжить работу?
Ответ прост — никак. По крайней мере, это нельзя сделать так же просто, как в других случаях. Нельзя просто так вернуть код ошибки вместо исключения, если функция на это не рассчитана.
Есть только одно решение: сделать две функции. Одна возвращает код ошибки, а вторая бросает исключения. Клиенты, которым нужны исключения, будут использовать второй вариант, остальные — первый.
Извините, что говорю такие очевидные вещи, но ради полноты изложения я должен был об этом сказать.
Для примера снова возьмём функцию выделения памяти. В этом случае я использую такие функции:
void* try_malloc(..., int &error_code) noexcept;
void* malloc(...);
При сбое выделения памяти первая версия возвращает nullptr и устанавливает error_code в коде ошибки. Вторая версия не возвращает nullptr, зато бросает исключение. Обратите внимание, что в рамках первой версии очень легко реализовать вторую:
void* malloc(...)
{
auto error_code = 0;
auto res = try_malloc(..., error_code);
if (!res)
throw malloc_error(error_code);
return res;
}
Не делайте этого в обратной последовательности, иначе вам придётся ловить исключение, а это дорого. Также это не даст нам скомпилировать код без включённой поддержки исключений. Если сделаете, как показано, то можете просто стереть другую перегрузку (overload) с помощью условного компилирования.
Но даже если у вас включена поддержка исключений, клиенту всё равно может понадобиться вторая версия. Например, когда нужно выделить наибольший возможный объём памяти, как в нашем примере. Будет проще и быстрее вызывать в цикле и проверять по условию, чем ловить исключение.
Решение 2: предоставить две перегрузки
Если недостаточно обработчика исключений, то нужно предоставить две перегрузки. Одна использует код возврата, а вторая бросает исключение.
Если рассматриваемая функция не имеет возвращаемого значения, то можете её использовать для кода ошибки. В противном случае вам придётся возвращать недопустимое значение для сигнализирования об ошибке — как nullptr в вышеприведённом примере, — а также установить выходной параметр для кода ошибки, если хотите предоставить вызывающему дополнительную информацию.
Пожалуйста, не используйте глобальную переменную errno или что-то типа GetLastError()!
Если возвращаемое значение не содержит недопустимое значение для обозначения сбоя, то по мере возможности используйте std::optional или что-то похожее.
Перегрузка исключения (exception overload) может — и должна — быть реализована в рамках версии с кодом ошибки, как это показано выше. Если компилируете без исключений, сотрите перегрузку с помощью условного компилирования.
std::system_error
Подобная система идеально подходит для работы с кодами ошибок в С++ 11.
Она возвращает непортируемый (non-portable) код ошибки std::error_code, то есть возвращаемый функцией операционной системы. С помощью сложной системы библиотечных средств и категорий ошибок вы можете добавить собственные коды ошибок, или портируемые std::error_condition. Для начала почитайте об этом здесь. Если нужно, то можете использовать в функции кода ошибки std::error_code. А для функции исключения есть подходящий класс исключения: std::system_error. Он берёт std::error_code и применяется для передачи этих ошибок в виде исключений.
Эту или подобную систему должны использовать все низкоуровневые функции, являющиеся закрытыми обёртками ОС-функций. Это хорошая — хотя и сложная — альтернатива службе кодов ошибок, предоставляемой операционной системой.
Да, и мне ещё нужно добавить подобное в функции виртуальной памяти. На сегодняшний день они не предоставляют коды ошибок.
std::expected
Выше упоминалось о проблеме, когда у вас нет возвращаемого значения, содержащего недопустимое значение, которое можно использовать для сигнализирования об ошибке. Более того, выходной параметр — не лучший способ получения кода ошибки.
А глобальные переменные вообще не вариант!
В № 4109 предложено решение: std::expected. Это шаблон класса, который также хранит возвращаемое значение или код ошибки. В вышеприведённом примере он мог бы использоваться так:
std::expected<void*, std::error_code> try_malloc(...);
В случае успеха std::expected будет хранить не-null указатель памяти, а при сбое — std::error_code. Сейчас эта методика работает при любых возвращаемых значениях. Комбинация std::expected и функции исключения определённо допускает любые варианты использования.
Заключение
Если вы создаёте библиотеки, то иногда приходится обеспечивать максимальную гибкость использования. Под этим подразумевается и разнообразие средств обработки ошибок: иногда требуются коды возврата, иногда — исключения.
Одна из возможных стратегий — улаживание этих противоречий с помощью обработчика исключений. Просто удостоверьтесь, что когда нужно, то вызывается callback, а не бросается исключение. Это замена для критических ошибок, которая в любом случае будет журналироваться перед прерыванием работы программы. Как таковой этот способ не универсален, вы не можете переключаться в одной программе между двумя версиями. Это лишь обходное решение при отключённой поддержке исключений.
Более гибкий подход — просто предоставить две перегрузки, одну с исключениями, а вторую без. Это даст пользователям максимальную свободу, они смогут выбирать ту версию, что лучше подходит в их ситуации. Недостаток этого подхода: вам придётся больше потрудиться при создании библиотеки.
В C++ различают ошибки времени компиляции и ошибки времени выполнения. Ошибки первого типа обнаруживает компилятор до запуска программы. К ним относятся, например, синтаксические ошибки в коде. Ошибки второго типа проявляются при запуске программы. Примеры ошибок времени выполнения: ввод некорректных данных, некорректная работа с памятью, недостаток места на диске и т. д. Часто такие ошибки могут привести к неопределённому поведению программы.
Некоторые ошибки времени выполнения можно обнаружить заранее с помощью проверок в коде. Например, такими могут быть ошибки, нарушающие инвариант класса в конструкторе. Обычно, если ошибка обнаружена, то дальнейшее выполение функции не имеет смысла, и нужно сообщить об ошибке в то место кода, откуда эта функция была вызвана. Для этого предназначен механизм исключений.
Коды возврата и исключения
Рассмотрим функцию, которая считывает со стандартного потока возраст и возвращает его вызывающей стороне. Добавим в функцию проверку корректности возраста: он должен находиться в диапазоне от 0 до 128 лет. Предположим, что повторный ввод возраста в случае ошибки не предусмотрен.
int ReadAge() {
int age;
std::cin >> age;
if (age < 0 || age >= 128) {
// Что вернуть в этом случае?
}
return age;
}
Что вернуть в случае некорректного возраста? Можно было бы, например, договориться, что в этом случае функция возвращает ноль. Но тогда похожая проверка должна быть и в месте вызова функции:
int main() {
if (int age = ReadAge(); age == 0) {
// Произошла ошибка
} else {
// Работаем с возрастом age
}
}
Такая проверка неудобна. Более того, нет никакой гарантии, что в вызывающей функции программист вообще её напишет. Фактически мы тут выбрали некоторое значение функции (ноль), обозначающее ошибку. Это пример подхода к обработке ошибок через коды возврата. Другим примером такого подхода является хорошо знакомая нам функция main. Только она должна возвращать ноль при успешном завершении и что-либо ненулевое в случае ошибки.
Другим способом сообщить об обнаруженной ошибке являются исключения. С каждым сгенерированным исключением связан некоторый объект, который как-то описывает ошибку. Таким объектом может быть что угодно — даже целое число или строка. Но обычно для описания ошибки заводят специальный класс и генерируют объект этого класса:
#include <iostream>
struct WrongAgeException {
int age;
};
int ReadAge() {
int age;
std::cin >> age;
if (age < 0 || age >= 128) {
throw WrongAgeException(age);
}
return age;
}
Здесь в случае ошибки оператор throw генерирует исключение, которое представлено временным объектом типа WrongAgeException. В этом объекте сохранён для контекста текущий неправильный возраст age. Функция досрочно завершает работу: у неё нет возможности обработать эту ошибку, и она должна сообщить о ней наружу. Поток управления возвращается в то место, откуда функция была вызвана. Там исключение может быть перехвачено и обработано.
Перехват исключения
Мы вызывали нашу функцию ReadAge из функции main. Обработать ошибку в месте вызова можно с помощью блока try/catch:
int main() {
try {
age = ReadAge(); // может сгенерировать исключение
// Работаем с возрастом age
} catch (const WrongAgeException& ex) { // ловим объект исключения
std::cerr << "Age is not correct: " << ex.age << "n";
return 1; // выходим из функции main с ненулевым кодом возврата
}
// ...
}
Мы знаем заранее, что функция ReadAge может сгенерировать исключение типа WrongAgeException. Поэтому мы оборачиваем вызов этой функции в блок try. Если происходит исключение, для него подбирается подходящий catch-обработчик. Таких обработчиков может быть несколько. Можно смотреть на них как на набор перегруженных функций от одного аргумента — объекта исключения. Выбирается первый подходящий по типу обработчик и выполняется его код. Если же ни один обработчик не подходит по типу, то исключение считается необработанным. В этом случае оно пробрасывается дальше по стеку — туда, откуда была вызвана текущая функция. А если обработчик не найдётся даже в функции main, то программа аварийно завершается.
Усложним немного наш пример, чтобы из функции ReadAge могли вылетать исключения разных типов. Сейчас мы проверяем только значение возраста, считая, что на вход поступило число. Но предположим, что поток ввода досрочно оборвался, или на входе была строка вместо числа. В таком случае конструкция std::cin >> age никак не изменит переменную age, а лишь возведёт специальный флаг ошибки в объекте std::cin. Наша переменная age останется непроинициализированной: в ней будет лежать неопределённый мусор. Можно было бы явно проверить этот флаг в объекте std::cin, но мы вместо этого включим режим генерации исключений при таких ошибках ввода:
int ReadAge() {
std::cin.exceptions(std::istream::failbit);
int age;
std::cin >> age;
if (age < 0 || age >= 128) {
throw WrongAgeException(age);
}
return age;
}
Теперь ошибка чтения в операторе >> у потока ввода будет приводить к исключению типа std::istream::failure. Функция ReadAge его не обрабатывает. Поэтому такое исключение покинет пределы этой функции. Поймаем его в функции main:
int main() {
try {
age = ReadAge(); // может сгенерировать исключения разных типов
// Работаем с возрастом age
} catch (const WrongAgeException& ex) {
std::cerr << "Age is not correct: " << ex.age << "n";
return 1;
} catch (const std::istream::failure& ex) {
std::cerr << "Failed to read age: " << ex.what() << "n";
return 1;
} catch (...) {
std::cerr << "Some other exceptionn";
return 1;
}
// ...
}
При обработке мы воспользовались функцией ex.what у исключения типа std::istream::failure. Такие функции есть у всех исключений стандартной библиотеки: они возвращают текстовое описание ошибки.
Обратите внимание на третий catch с многоточием. Такой блок, если он присутствует, будет перехватывать любые исключения, не перехваченные ранее.
Исключения стандартной библиотеки
Функции и классы стандартной библиотеки в некоторых ситуациях генерируют исключения особых типов. Все такие типы выстроены в иерархию наследования от базового класса std::exception. Иерархия классов позволяет писать обработчик catch сразу на группу ошибок, которые представлены базовым классом: std::logic_error, std::runtime_error и т. д.
Вот несколько примеров:
-
Функция
atу контейнеровstd::array,std::vectorиstd::dequeгенерирует исключениеstd::out_of_rangeпри некорректном индексе. -
Аналогично, функция
atуstd::map,std::unordered_mapи у соответствующих мультиконтейнеров генерирует исключениеstd::out_of_rangeпри отсутствующем ключе. -
Обращение к значению у пустого объекта
std::optionalприводит к исключениюstd::bad_optional_access. -
Потоки ввода-вывода могут генерировать исключение
std::ios_base::failure.
Исключения в конструкторах
В главе 3.1 мы написали класс Time. Этот класс должен был соблюдать инвариант на значение часов, минут и секунд: они должны были быть корректными. Если на вход конструктору класса Time передавались некорректные значения, мы приводили их к корректным, используя деление с остатком.
Более правильным было бы сгенерировать в конструкторе исключение. Таким образом мы бы явно передали сообщение об ошибке во внешнюю функцию, которая пыталась создать объект.
class Time {
private:
int hours, minutes, seconds;
public:
// Заведём класс для исключения и поместим его внутрь класса Time как в пространство имён
class IncorrectTimeException {
};
Time::Time(int h, int m, int s) {
if (s < 0 || s > 59 || m < 0 || m > 59 || h < 0 || h > 23) {
throw IncorrectTimeException();
}
hours = h;
minutes = m;
seconds = s;
}
// ...
};
Генерировать исключения в конструкторах — совершенно нормальная практика. Однако не следует допускать, чтобы исключения покидали пределы деструкторов. Чтобы понять причины, посмотрим подробнее, что происходит при генерации исключения.
Свёртка стека
Вспомним класс Logger из предыдущей главы. Посмотрим, как он ведёт себя при возникновении исключения. Воспользуемся в этом примере стандартным базовым классом std::exception, чтобы не писать свой класс исключения.
#include <exception>
#include <iostream>
void f() {
std::cout << "Welcome to f()!n";
Logger x;
// ...
throw std::exception(); // в какой-то момент происходит исключение
}
int main() {
try {
Logger y;
f();
} catch (const std::exception&) {
std::cout << "Something happened...n";
return 1;
}
}
Мы увидим такой вывод:
Logger(): 1 Welcome to f()! Logger(): 2 ~Logger(): 2 ~Logger(): 1 Something happened...
Сначала создаётся объект y в блоке try. Затем мы входим в функцию f. В ней создаётся объект x. После этого происходит исключение. Мы должны досрочно покинуть функцию. В этот момент начинается свёртка стека (stack unwinding): вызываются деструкторы для всех созданных объектов в самой функции и в блоке try, как если бы они вышли из своей области видимости. Поэтому перед обработчиком исключения мы видим вызов деструктора объекта x, а затем — объекта y.
Аналогично, свёртка стека происходит и при генерации исключения в конструкторе. Напишем класс с полем Logger и сгенерируем нарочно исключение в его конструкторе:
#include <exception>
#include <iostream>
class C {
private:
Logger x;
public:
C() {
std::cout << "C()n";
Logger y;
// ...
throw std::exception();
}
~C() {
std::cout << "~C()n";
}
};
int main() {
try {
C c;
} catch (const std::exception&) {
std::cout << "Something happened...n";
}
}
Вывод программы:
Logger(): 1 // конструктор поля x C() Logger(): 2 // конструктор локальной переменной y ~Logger(): 2 // свёртка стека: деструктор y ~Logger(): 1 // свёртка стека: деструктор поля x Something happened...
Заметим, что деструктор самого класса C не вызывается, так как объект в конструкторе не был создан.
Механизм свёртки стека гарантирует, что деструкторы для всех созданных автоматических объектов или полей класса в любом случае будут вызваны. Однако он полагается на важное свойство: деструкторы самих классов не должны генерировать исключений. Если исключение в деструкторе произойдёт в момент свёртки стека при обработке другого исключения, то программа аварийно завершится.
Пример с динамической памятью
Подчеркнём, что свёртка стека работает только с автоматическими объектами. В этом нет ничего удивительного: ведь за временем жизни объектов, созданных в динамической памяти, программист должен следить самостоятельно. Исключения вносят дополнительные сложности в ручное управление динамическими объектами:
void f() {
Logger* ptr = new Logger(); // конструируем объект класса Logger в динамической памяти
// ...
g(); // вызываем какую-то функцию
// ...
delete ptr; // вызываем деструктор и очищаем динамическую память
}
На первый взгляд кажется, что в этом коде нет ничего опасного: delete вызывается в конце функции. Однако функция g может сгенерировать исключение. Мы не перехватываем его в нашей функции f. Механизм свёртки уберёт со стека лишь сам указатель ptr, который является автоматической переменной примитивного типа. Однако он ничего не сможет сделать с объектом в памяти, на которую ссылается этот указатель. В логе мы увидим только вызов конструктора класса Logger, но не увидим вызова деструктора. Нам придётся обработать исключение вручную:
void f() {
Logger* ptr = new Logger();
// ...
try {
g();
} catch (...) { // ловим любое исключение
delete ptr; // вручную удаляем объект
throw; // перекидываем объект исключения дальше
}
// ...
delete ptr;
}
Здесь мы перехватываем любое исключение и частично обрабатываем его, удаляя объект в динамической памяти. Затем мы прокидываем текущий объект исключения дальше с помощью оператора throw без аргументов.
Согласитесь, этот код очень далёк от совершенства. При непосредственной работе с объектами в динамической памяти нам приходится оборачивать в try/catch любую конструкцию, из которой может вылететь исключение. Понятно, что такой код чреват ошибками. В главе 3.6 мы узнаем, как с точки зрения C++ следует работать с такими ресурсами, как память.
Гарантии безопасности исключений
Предположим, что мы пишем свой класс-контейнер, похожий на двусвязный список. Наш контейнер позволяет добавлять элементы в хранилище и отдельно хранит количество элементов в некотором поле elementsCount. Один из инвариантов этого класса такой: значение elementsCount равно реальному числу элементов в хранилище.
Не вдаваясь в детали, давайте посмотрим, как могла бы выглядеть функция добавления элемента.
template <typename T>
class List {
private:
struct Node { // узел двусвязного списка
T element;
Node* prev = nullptr; // предыдущий узел
Node* next = nullptr; // следующий узел
};
Node* first = nullptr; // первый узел списка
Node* last = nullptr; // последний узел списка
int elementsCount = 0;
public:
// ...
size_t Size() const {
return elementsCount;
}
void PushBack(const T& elem) {
++elementsCount;
// Конструируем в динамической памяти новой узел списка
Node* node = new Node(elem, last, nullptr);
// Связываем новый узел с остальными узлами
if (last != nullptr) {
last->next = node;
} else {
first = node;
}
last = node;
}
};
Не будем здесь рассматривать другие функции класса — конструкторы, деструктор, оператор присваивания… Рассмотрим функцию PushBack. В ней могут произойти такие исключения:
-
Выражение
newможет сгенерировать исключениеstd::bad_allocиз-за нехватки памяти. -
Конструктор копирования класса
Tможет сгенерировать произвольное исключение. Этот конструктор вызывается при инициализации поляelementсоздаваемого узла в конструкторе классаNode. В этом случаеnewведёт себя как транзакция: выделенная перед этим динамическая память корректно вернётся системе.
Эти исключения не перехватываются в функции PushBack. Их может перехватить код, из которого PushBack вызывался:
#include <iostream>
class C; // какой-то класс
int main() {
List<C> data;
C element;
try {
data.PushBack(element);
} catch (...) { // не получилось добавить элемент
std::cout << data.Size() << "n"; // внезапно 1, а не 0
}
// работаем дальше с data
}
Наша функция PushBack сначала увеличивает счётчик элементов, а затем выполняет опасные операции. Если происходит исключение, то в классе List нарушается инвариант: значение счётчика elementsCount перестаёт соответствовать реальности. Можно сказать, что функция PushBack не даёт гарантий безопасности.
Всего выделяют четыре уровня гарантий безопасности исключений (exception safety guarantees):
-
Гарантия отсутствия сбоев. Функции с такими гарантиями вообще не выбрасывают исключений. Примерами могут служить правильно написанные деструктор и конструктор перемещения, а также константные функции вида
Size. -
Строгая гарантия безопасности. Исключение может возникнуть, но от этого объект нашего класса не поменяет состояние: количество элементов останется прежним, итераторы и ссылки не будут инвалидированы и т. д.
-
Базовая гарантия безопасности. При исключении состояние объекта может поменяться, но оно останется внутренне согласованным, то есть, инварианты будут соблюдаться.
-
Отсутсвие гарантий. Это довольно опасная категория: при возникновении исключений могут нарушаться инварианты.
Всегда стоит разрабатывать классы, обеспечивающие хотя бы базовую гарантию безопасности. При этом не всегда возможно эффективно обеспечить строгую гарантию.
Переместим в нашей функции PushBack изменение счётчика в конец:
void PushBack(const T& elem) {
Node* node = new Node(elem, last, nullptr);
if (last != nullptr) {
last->next = node;
} else {
first = node;
}
last = node;
++elementsCount; // выполнится только если раньше не было исключений
}
Теперь такая функция соответствует строгой гарантии безопасности.
В документации функций из классов стандартной библиотеки обычно указано, какой уровень гарантии они обеспечивают. Рассмотрим, например, гарантии безопасности класса std::vector.
-
Деструктор, функции
empty,size,capacity, а такжеclearпредоставляют гарантию отсутствия сбоев. -
Функции
push_backиresizeпредоставляют строгую гарантию. -
Функция
insertпредоставляет лишь базовую гарантию. Можно было бы сделать так, чтобы она предоставляла строгую гарантию, но за это пришлось бы заплатить её эффективностью: при вставке в середину вектора пришлось бы делать реаллокацию.
Функции класса, которые гарантируют отсутсвие сбоев, следует помечать ключевым словом noexcept:
class C {
public:
void f() noexcept {
// ...
}
};
С одной стороны, эта подсказка позволяет компилятору генерировать более эффективный код. С другой — эффективно обрабатывать объекты таких классов в стандартных контейнерах. Например, std::vector<C> при реаллокации будет использовать конструктор перемещения класса C, если он помечен как noexcept. В противном случае будет использован конструктор копирования, который может быть менее эффективен, но зато позволит обеспечить строгую гарантию безопасности при реаллокации.
Глава 9
Обработка исключений
Основные навыки и понятия
- Представление об иерархии исключений
- Использование ключевых слов try и catch
- Последствия неперехвата исключений
- Применение нескольких операторов catch
- Перехват исключений, генерируемых подклассами
- Вложенные блоки try
- Генерирование исключений
- Представление о членах класса Throwable
- Использование ключевого слова finally
- Использование ключевого слова throws
- Представление о исключениях, встроенные в Java
- Создание специальных классов исключений
В этой главе речь пойдет об обработке исключительный ситуаций, или просто исключений. Исключение — это ошибка, возникающая в процессе выполнения программы. Используя подсистему обработки исключений Java, можно контролировать реакцию программы на появление ошибок в ходе ее выполнения. Средства обработки исключений в том или ином виде присутствуют практически во всех современных языках программирования. Можно смело утверждать, что в Java подобные инструментальные средства отличаются большей гибкостью, более понятны и удобны в употреблении по сравнению с большинством других языков программирования.
Преимущество обработки исключений заключается в том, что она автоматически предусматривает реакцию на многие ошибки, избавляя от необходимости писать вручную соответствующий код. Например, в некоторых старых языках программирования предусматривается возврат специального кода при возникновении ошибки в ходе выполнения метода. Этот код приходится проверять вручную при каждом вызове метода. Такой подход к обработке ошибок вручную трудоемок и чреват погрешностями. Обработка исключений упрощает этот процесс, давая возможность определять в программе блок кода, называемый обработчиком исключения и автоматически выполняющийся при возникновении ошибки. Это избавляет от необходимости проверять вручную, насколько удачно или неудачно была выполнена та или иная операция или вызов метода. Если возникнет ошибка, все необходимые действия по ее обработке выполнит обработчик исключений.
В Java определены стандартные исключения для наиболее часто встречающихся программных ошибок, в том числе деления на нуль или попытки открыть несуществующий файл. Для того чтобы обеспечить требуемую реакцию на конкретную ошибку, в программу следует включить соответствующий обработчик событий. Исключения широко применяются в библиотеке Java API.
Иерархия исключений
В Java все исключения представлены отдельными классами. Все классы исключений являются потомками класса Throwable. Так, если в программе возникнет исключительная ситуация, будет сгенерирован объект класса, соответствующего определенному типу исключения. У класса Throwable имеются два непосредственных подкласса: Exception и Error. Исключения типа Error относятся к ошибкам, возникающим в виртуальной машине Java, а не в прикладной программе. Контролировать такие исключения невозможно, поэтому реакция на них в прикладной программе, как правило, не предусматривается. В связи с этим исключения данного типа не будут описываться в этой книге.
Ошибки, связанные с выполнением действий в программе, представлены отдельными подклассами, производными от класса Exception. К этой категории, в частности, относятся ошибки деления на нуль, выхода за границы массива и обращения к файлам. Подобные ошибки следует обрабатывать в самой программе. Важным подклассом, производным от Exception, является класс RuntimeException, который служит для представления различных видов ошибок, часто встречающихся при выполнении программ.
Общее представление об обработке исключений
Для обработки исключений в Java предусмотрены пять ключевых слов: try, catch, throw, throws и finally. Они образуют единую подсистему, в которой использование одного ключевого слова почти всегда автоматически влечет за собой употребление другого. Каждое из упомянутых выше ключевых слов будет подробно рассмотрено далее в этой главе. Но прежде следует дать общее представление об их роли в процессе обработки исключений. Поэтому ниже поясняется вкратце, каким образом они действуют.
Операторы, в которых требуется отслеживать появление исключений, помещаются в блок try. Если в блоке try будет сгенерировано исключение, его можно перехватить и обработать нужным образом. Системные исключения генерируются автоматически. А для того чтобы сгенерировать исключение вручную, следует воспользоваться ключевым словом throw. Иногда возникает потребность обрабатывать исключения за пределами метода, в котором они возникают, и для этой цели служит ключевое слово throws. Если же некоторый фрагмент кода должен быть выполнен обязательно и независимо от того, возникнет исключение или нет, его следует поместить в блок finally.
Использование ключевых слов try и catch
Основными языковыми средствами обработки исключений являются ключевые слова try и catch. Они используются совместно. Это означает, что нельзя указать ключевое слово catch в коде, не указав ключевое слово try. Ниже приведена общая форма записи блоков try/catch, предназначенных для обработки исключений.
try {
// Блок кода, в котором должны отслеживаться ошибки
}
catch (тип_исключения_1 объект_исключения) {
// Обработчик исключения тип_исключения_1
}
catch (тип_исключения_2 объект_исключения) {
// Обработчик исключения тип_исключения_2
}
В скобках, следующих за ключевым словом catch, указываются тип исключения и переменная, ссылающаяся на объект данного типа. Когда возникает исключение, оно перехватывается соответствующим оператором catch, обрабатывающим это исключение. Как следует из приведенной выше общей формы записи, с одним блоком try может быть связано несколько операторов catch. Тип исключения определяет, какой именно оператор catch будет выполняться. Так, если тип исключения соответствует типу оператора catch, то именно он и будет выполнен, а остальные операторы catch — пропущены. При перехвате исключения переменной, указанной в скобках после ключевого слова catch, присваивается ссылка на объект_исключения.
Следует иметь в виду, что если исключение не генерируется, блок try завершается обычным образом и ни один из его операторов catch не выполняется. Выполнение программы продолжается с первого оператора, следующего за последним оператором catch. Таким образом, операторы catch выполняются только при появлении исключения.
На заметку.
В версии JDK 7 внедрена новая форма оператора try, поддерживающая автоматическое управления ресурсами и называемая оператором try с ресурсами. Более подробно она описывается в главе 10 при рассмотрении потоков ввода-вывода, в том числе и тех, что связаны с файлами, поскольку потоки ввода-вывода относятся к числу ресурсов, наиболее употребительных в прикладных программах.
Простой пример обработки исключений
Рассмотрим простой пример, демонстрирующий перехват и обработку исключения. Как известно, попытка обратиться за границы массива приводит к ошибке, и виртуальная машина Java генерирует соответствующее исключение ArraylndexOutOf BoundsException. Ниже приведен код программы, в которой намеренно создаются условия для появления данного исключения, которое затем перехватывается.
// Демонстрация обработки исключений,
class ExcDemol {
public static void main (String args[]) {
int nums[] = new int[4];
// Создание блока try.
try {
System.out.println("Before exception is generated.");
// Попытка обратиться за границы массива.
nums[7] = 10;
System.out.println("this won't be displayed");
}
// Перехват исключения в связи с обращением за границы массива.
catch (ArraylndexOutOfBoundsException exc) {
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
Результат выполнения данной программы выглядит следующим образом:
Before exception is generated.
Index out-of-bounds!
After catch statement.
Несмотря на всю простоту данного примера программы, он наглядно демонстрирует несколько важных особенностей обработки исключений. Во-первых, код, подлежащий проверке на наличие ошибок, помещается в блок try. И во-вторых, когда возникает исключение (в данном случае это происходит при попытке обратиться за границы массива), выполнение блока try прерывается и управление получает блок catch. Следовательно, явного обращения к блоку catch не происходит, но переход к нему осуществляется лишь при определенном условии, возникающем в ходе выполнения программы. Так, оператор вызова метода println(), следующий за выражением, в котором происходит обращение к несуществующему элементу массива, вообще не выполняется. По завершении блока catch выполнение программы продолжается с оператора, следующего за этим блоком. Таким образом, обработчик исключений предназначен для устранения программных ошибок, приводящих к исключительным ситуациям, а также для обеспечения нормального продолжения исполняемой программы.
Как упоминалось выше, если в блоке try не возникнут исключения, операторы в блоке catch не получат управление и выполнение программы продолжится после блока catch. Для того чтобы убедиться в этом, измените в предыдущей программе строку кода
на следующую строку кода:
Теперь исключение не возникнет и блок catch не выполнится.
Важно понимать, что исключения отслеживаются во всем коде в блоке try. К их числу относятся исключения, которые могут быть сгенерированы методом, вызываемым из блока try. Исключения, возникающие в вызываемом методе, перехватываются операторами в блоке catch, связанном с блоком try. Правда, это произойдет лишь в том случае, если метод не обрабатывает исключения самостоятельно. Рассмотрим в качестве примера следующую программу:
/* Исключение может быть сгенерировано одним методом,
а перехвачено другим. */
class ExcTest {
// сгенерировать исключение
static void genException() {
int nums[] = new int[4];
System.out.println("Before exception is generated.");
// Здесь генерируется исключение в связи с
// обращением за границы массива.
nums[7] = 10;
System.out.println("this won't be displayed");
}
}
class ExcDemo2 {
public static void main(String args[]) {
try {
ExcTest.genException() ;
}
//А здесь исключение перехватывается.
catch (ArraylndexOutOfBoundsException exc) {
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
Выполнение этой версии программы дает такой же результат, как и при выполнении ее предыдущей версии. Этот результат приведен ниже.
Before exception is generated.
Index out-of-bounds!
After catch statement.
Метод genException() вызывается из блока try, и поэтому генерируемое, но не перехватываемое в нем исключение перехватывается далее в блоке catch в методе main(). Если бы метод genException() сам перехватывал исключение, оно вообще не дошло бы до метода main().
Последствия неперехвата исключений
Перехват стандартного исключения Java, продемонстрированный в предыдущем примере, позволяет предотвратить завершение программы вследствие ошибки. Генерируемое исключение должно быть перехвачено и обработано. Если исключение не обрабатывается в программе, оно будет обработано виртуальной машиной Java. Но дело в том, что по умолчанию виртуальная машина Java аварийно завершает программу, выводя сообщение об ошибке и трассировку стека исключений. Допустим, в предыдущем примере попытка обращения за границы массива не отслеживается и исключение не перехватывается, как показано ниже.
// Обработка ошибки средствами виртуальной машины Java,
class NotHandled {
public static void main(String args[]) {
int nums[] = new int[4];
System.out.println("Before exception is generated.");
// Попытка обращения за границы массива,
nums[7] = 10;
}
}
При появлении ошибки, связанной с обращением за границы массива, выполнение программы прекращается и выводится следующее сообщение:
Exception in thread "main" java.lang.ArraylndexOutOfBoundsException: 7 at NotHandled.main(NotHandled.java:9)
Оно полезно на этапе отладки, но пользователям программы эта информация вряд ли нужна. Именно поэтому очень важно, чтобы программы обрабатывали исключения самостоятельно и не поручали эту задачу виртуальной машине Java.
Как упоминалось выше, тип исключения должен соответствовать типу, указанному в операторе catch. В противном случае исключение не будет перехвачено. Так, в приведенном ниже примере программы делается попытка перехватить исключение, связанное с обращением за границы массива, с помощью оператора catch, в котором указан тип ArithmeticException еще одного встроенного в Java исключения. При неправильном обращении к массиву будет сгенерировано исключение ArraylndexOutOfBoundsException, не соответствующее типу, указанному в операторе catch. В результате программа будет завершена аварийно.
// Эта программа не будет работать нормально!
class ExcTypeMismatch {
public static void main(String args[]) {
int nums[] = new int[4];
try {
System.out.println("Before exception is generated.");
// При выполнении следующего оператора возникает
// исключение ArraylndexOutOfBoundsException
nums[7] = 10;
System.out.println("this won’t be displayed");
}
/* Исключение, связанное с обращением за границы массива,
нельзя обработать с помощью оператора catch, в котором
указан тип исключения ArithmeticException. */
catch (ArithmeticException exc) {
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
Ниже приведен результат выполнения данной программы.
Before exception is generated.
Exception in thread "main" java.lang.ArraylndexOutOfBoundsException: 7
at ExcTypeMismatch.main(ExcTypeMismatch.java:10)
Нетрудно заметить, что оператор catch, в котором указан тип исключения ArithmeticException, не может перехватить исключение ArraylndexOutOfBoundsException.
Обработка исключений — изящный способ устранения программных ошибок
Одно из главных преимуществ обработки исключений заключается в том, что она позволяет вовремя отреагировать на ошибку в программе и затем продолжить ее выполнение. В качестве примера рассмотрим еще одну программу, в которой элементы одного массива делятся на элементы другого. Если при этом происходит деление на нуль, то генерируется исключение ArithmeticException. Обработка подобного исключения заключается в том, что программа уведомляет об ошибке и затем продолжает свое выполнение. Таким образом, попытка деления на нуль не приведет к аварийному завершению программы из-за ошибки при ее выполнении. Вместо этого ошибка обрабатывается изящно, не прерывая выполнение программы.
// Изящная обработка исключения и продолжение выполнения программы,
class ExcDemo3 {
public static void main(String args[]) {
int numer[] = { 4, 8, 16, 32, 64, 128 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can't divide by Zero!");
}
}
}
}
Результат выполнения данной программы выглядит следующим образом:
4 / 2 is 2
Can't divide by Zero!
16/ 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16
Данный пример демонстрирует еще одну важную особенность: обработанное исключение удаляется из системы. Иными словами, на каждом шаге цикла блок try выполняется в программе сызнова, а все возникшие ранее исключения считаются обработанными. Благодаря этому в программе могут обрабатываться повторяющиеся ошибки.
Применение нескольких операторов catch
Как пояснялось ранее, с блоком try можно связать несколько операторов catch. Обычно разработчики так и поступают на практике. Каждый из операторов catch должен перехватывать отдельный тип исключений. Например, в приведенной ниже программе обрабатываются как исключения, связанные с обращением за границы массива, так и ошибки деления на нуль.
// Применение нескольких операторов catch. '
class ExcDemo4 {
public static void main(String args[]) {
// Здесь массив numer длиннее массива denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
// За блоком try следует несколько блоков catch подряд,
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can't divide by Zero!");
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
}
}
}
}
Выполнение этой программы дает следующий результат:
4 / 2 is 2
Can't divide by Zero!
16 / 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16
No matching element found.
No matching element found.
Как подтверждает приведенный выше результат выполнения программы, в каждом блоке оператора catch обрабатывается свой тип исключения.
Вообще говоря, выражения с операторами catch проверяются в том порядке, в котором они встречаются в программе. И выполняется лишь тот из них, который соответствует типу возникшего исключения. А остальные блоки операторов catch просто игнорируются.
Перехват исключений, генерируемых подклассами
В отношении подклассов следует отметить еще одну интересную особенность применения нескольких операторов catch: условие перехвата исключений для суперкласса будет справедливо и для любых его подклассов. Например, класс Throwable является суперклассом для всех исключений, поэтому для перехвата всех возможных исключений в операторах catch следует указывать тип Throwable. Если же требуется перехватывать исключения типа суперкласса и типа подкласса, то в блоке операторов первым должен быть указан тип исключения, генерируемого подклассом. В противном случае вместе с исключением типа суперкласса будут перехвачены и все исключения производных от него классов. Это правило соблюдается автоматически, и если указать первым тип исключения, генерируемого суперклассом, то будет создан недостижимый код, поскольку условие перехвата исключения, генерируемого подклассом, никогда не будет выполнено. А ведь недостижимый код в Java считается ошибкой.
Рассмотрим в качестве примера следующую программу
//В операторах catch исключения типа подкласса должны
// предшествовать исключениям типа суперкласса,
class ExcDemo5 {
public static void main(String args[]) {
// Здесь массив numer длиннее массива denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
// Перехват исключения от подкласса.
catch (ArraylndexOutOfBoundsException exc) {
System.out.println("No matching element found.");
}
// Перехват исключения от суперкласса.
catch (Throwable exc) {
System.out.println("Some exception occurred.");
}
}
}
}
Ниже приведен результат выполнения данной программы.
4 / 2 is 2
Some exception occurred.
16 / 4 is 4
32 / 4 is 8
Some exception occurred.
128 / 8 is 16
No matching element found.
No matching element found.
В данном случае оператор catch (Throwable) перехватывает все исключения, кроме ArraylndexOutOfBoundsException. Соблюдение правильного порядка следования операторов catch приобретает особое значение в том случае, когда исключения генерируются в самой программе.
Вложенные блоки try
Блоки try могут быть вложенными друг в друга. Исключение, возникшее во внутреннем блоке try и не перехваченное связанным с ним блоком catch, распростра¬няется далее во внешний блок try и обрабатывается связанным с ним блоком catch. Такой порядок обработки исключений демонстрируется в приведенном ниже примере программы, где исключение ArraylndexOutOfBoundsException не перехватывается во внутреннем блоке catch, но обрабатывается во внешнем.
// Применение вложенных блоков try.
class NestTrys {
public static void main(String args[]) {
// Массив numer длиннее, чем массив denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
// Вложенные блоки try.
try { // Внешний блок try.
for(int i=0; i<numer.length; i++) {
try { // Внутренний блок try.
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]) ;
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can't divide by Zero!");
}
}
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
System.out.println("Fatal error - program terminated.");
}
}
}
Выполнение этой программы может дать, например, следующий результат:
4 / 2 is 2
Can't divide by Zero!
16 / 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16
No matching element found.
Fatal error - program terminated.
В данном примере исключение, которое может быть обработано во внутреннем блоке try (в данном случае ошибка деления на нуль), не мешает дальнейшему выполнению программы. А вот ошибка превышения границ массива перехватывается во внешнем блоке try, что приводит к аварийному завершению программы.
Ситуация, продемонстрированная в предыдущем примере, является не единственной причиной для применения вложенных блоков try, хотя она встречается очень часто. В этом случае вложенные блоки try помогают по-разному обрабатывать разные типы ошибок. Одни ошибки невозможно устранить, а для других достаточно предусмотреть сравнительно простые действия. Внешний блок try чаще всего используется для перехвата критических ошибок, а менее серьезные ошибки обрабатываются во внутреннем блоке try.
Генерирование исключений
В предыдущих примерах программ обрабатывались исключения, автоматически генерируемые виртуальной машиной Java. Но генерировать исключения можно и вручную, используя для этого оператор throw. Ниже приведена общая форма этого оператора.
где объект_исключения должен быть объектом класса, производного от класса Throwable.
Ниже приведен пример программы, демонстрирующий применение оператора throw. В этой программе исключение ArithmeticException генерируется вручную.
// Генерирование исключения вручную,
class ThrowDemo {
public static void main(String args[]) {
try {
System.out.println("Before throw.");
// Генерирование исключения.
throw new ArithmeticException() ;
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Exception caught.");
}
System.out.println("After try/catch block.");
}
}
Выполнение этой программы дает следующий результат:
Before throw.
Exception caught.
After try/catch block.
Обратите внимание на то, что исключение ArithmeticException генерируется с помощью ключевого слова new в операторе throw. Дело в том, что оператор throw генерирует исключение в виде объекта. Поэтому после ключевого слова throw недостаточно указать только тип исключения, нужно еще создать объект для этой цели.
Повторное генерирование исключений
Исключение, перехваченное блоком catch, может быть повторно сгенерировано для обработки другим аналогичным блоком. Чаще всего повторное генерирование исключений применяется с целью предоставить разным обработчикам доступ к исключению. Так, например, повторное генерирование имеет смысл в том случае, если один обработчик оперирует одним свойством исключения, а другой обработчик ориентирован на другое его свойство. Повторно сгенерированное исключение не может быть перехвачено тем же самым блоком catch. Оно распространяется в другие блоки catch.
Ниже приведен пример программы, демонстрирующий повторное генерирование исключений.
//•Повторное генерирование исключений,
class Rethrow {
public static void genException() {
// Массив numer длиннее маесивв denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can11 divide by Zero!");
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
throw exc; // Повторное генерирование исключения.
}
}
}
}
class RethrowDemo {
public static void main(String args[]) {
try {
Rethrow.genException();
}
catch(ArraylndexOutOfBoundsException exc) {
// Перехват повторно сгенерированного включения.
System.out.println("Fatal error - " +
"program terminated.");
}
}
}
В данной программе ошибка деления на нуль обрабатывается локально в методе genException(), а при попытке обращения за границы массива исключение генерируется повторно. На этот раз оно перехватывается в методе main().
Подробнее о классе Throwable
В приведенных до сих примерах программ только перехватывались исключения, но не выполнялось никаких действий над представляющими их объектами. В выражении оператора catch указываются тип исключения и параметр, принимающий объект исключения. А поскольку все исключения представлены подклассами, производными от класса Throwable, то они поддерживают методы, определенные в этом классе. Некоторые наиболее употребительные методы из класса Throwable приведены в табл. 9.1.
Таблица 9.1. Наиболее употребительные методы из класса Throwable
| Метод | Описание |
|---|---|
Throwable filllnStackTrace() |
Возвращает объект типа Throwable, содержащий полную трассировку стека исключений. Этот объект пригоден для повторного генерирования исключений |
String getLocalizedMessage() |
Возвращает описание исключения, локализованное по региональным стандартам |
String getMessage() |
Возвращает описание исключения |
void printStackTrace() |
Выводит трассировку стека исключений |
void printStackTrace(PrintStream stream) |
Выводит трассировку стека исключений в указанный поток |
void printStackTrace(PrintWriter stream) |
Направляет трассировку стека исключений в указанный поток |
String toString() |
Возвращает объект типа String, содержащий полное описание исключения. Этот метод вызывается из метода println() при выводе объекта типа Throwable |
Среди методов, определенных в классе Throwable, наибольший интерес представляют методы pr intStackTrace() и toString(). С помощью метода printStackTrace() можно вывести стандартное сообщение об ошибке и запись последовательности вызовов методов, которые привели к возникновению исключения, А метод toString() позволяет получить стандартное сообщение об ошибке. Этот метод также вызывается в том случае, когда объект исключения передается в качестве параметра методу println(). Применение этих методов демонстрируется в следующем примере программы:
// Применение методов из класса Throwable.
class ExcTest {
static void genException() {
int nums[] = new int[4];
System.out.println("Before exception is generated.");
// сгенерировать исключение в связи с попыткой
// обращения за границы массива
nums[7] = 10;
System.out.println("this won't be displayed");
}
}
class UseThrowableMethods {
public static void main(String args[]) {
try {
ExcTest.genException() ;
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("Standard message is: ");
System.out.println(exc) ;
System.out.println("nStack trace: ");
exc.printStackTrace();
}
System.out.println("After catch statement.");
}
}
Результат выполнения данной программы выглядит следующим образом:
Before exception is generated.
Standard message is:
java.lang.ArraylndexOutOfBoundsException: 7
Stack trace:
java.lang.ArraylndexOutOfBoundsException: 7
at ExcTest.genException(UseThrowableMethods.java:10)
at UseThrowableMethods.main(UseThrowableMethods.java:19)
After catch statement.
Использование ключевого слова finally
Иногда требуется определить кодовый блок, который должен выполняться по завершении блока try/catch. Допустим, в процессе работы программы возникло исключение, требующее ее преждевременного завершения. Но в программе открыт файл или установлено сетевое соединение, а следовательно, файл нужно закрыть, а соединение разорвать. Для выполнения подобных операций нормального завершения программы удобно воспользоваться ключевым словом finally.
Для того чтобы определить код, который должен выполняться по завершении блока try/catch, нужно указать блок finally в конце последовательности операторов try/catch. Ниже приведена общая форма записи блока try/catch вместе с блоком finally.
try {
// Блок кода, в котором отслеживаются ошибки.
}
catch (тип_исключения_1 объект_исключения) {
// Обработчик исключения тип_исключения_1
}
catch (тип_исключения_2 объект_исключения) {
// Обработчик исключения тип_исключения_2
}
//. . .
finally {
// Код блока finally
}
Блок finally выполняется всегда по завершении блока try/catch независимо от того, какое именно условие к этому привело. Следовательно, блок finally получит управление как при нормальной работе программы, так и при возникновении ошибки. Более того, он будет вызван даже в том случае, если в блоке try или в одном из блоков catch будет присутствовать оператор return для немедленного возврата из метода.
Ниже приведен краткий пример программы, демонстрирующий применение блока finally.
// Применение блока finally,
class UseFinally {
public static void genException(int what) {
int t;
int nums[] = new int[2];
System.out.println("Receiving " + what);
try {
switch(what) {
case 0:
t = 10 / what; // сгенерировать ошибку деления на нуль
break;
case 1:
nums[4] = 4; // сгенерировать ошибку обращения к массиву
break;
case 2:
return; // возвратиться из блока try
}
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can1t divide by Zero!");
return; // возвратиться из блока catch
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
}
// Этот блок выполняется независимо от того, каким
// образом завершается блок try/catch.
finally {
System.out.println("Leaving try.");
}
}
}
class FinallyDemo {
public static void main(String args[]) {
for(int i=0; i < 3; i++) {
UseFinally.genException(i);
System.out.println() ;
}
}
}
В результате выполнения данной программы получается следующий результат:
Receiving О
Can't divide by Zero!
Leaving try.
Receiving 1
No matching element found.
Leaving try.
Receiving 2
Leaving try.
Нетрудно заметить, что блок finally выполняется независимо от того, каким об¬
разом завершается блок try/catch.
Использование ключевого слова throws
Иногда исключения нецелесообразно обрабатывать в том методе, в котором они возникают. В таком случае их следует указывать с помощью ключевого слова throws. Ниже приведена общая форма объявления метода, в котором присутствует ключевое слово throws.
возвращаемый_тип имя_метода(список_параметров) throws список_исключений {
// Тело метода
}
В списке исключений через запятую указываются исключения, которые может генерировать метод.
Возможно, вам покажется странным, что в ряде предыдущих примеров ключевое слово throws не указывалось при генерировании исключений за пределами методов. Дело в том, что исключения, генерируемые подклассом Error или RuntimeException, можно и не указывать в списке оператора throws. Исполняющая система Java по умолчанию предполагает, что метод может их генерировать. А исключения всех остальных типов следует непременно объявить с помощью ключевого слова throws. Если этого не сделать, возникнет ошибка при компиляции.
Пример применения оператора throws уже был представлен ранее в этой книге.
Напомним, что при организации ввода с клавиатуры в метод main() потребовалось
включить следующее выражение:
throws java.io.IOException
Теперь вы знаете, зачем это было нужно. При вводе данных может возникнуть исключение IOException, а на тот момент вы еще не знали, как оно обрабатывается. Поэтому мы и указали, что исключение должно обрабатываться за пределами метода main(). Теперь, ознакомившись с исключениями, вы сможете без труда обработать исключение IOException самостоятельно.
Рассмотрим пример, в котором осуществляется обработка исключения IOException. В методе prompt() отображается сообщение, а затем выполняется ввод символов с клавиатуры. Такой ввод данных может привести к возникновению исключения IOException. Но это исключение не обрабатывается в методе prompt(). Вместо этого в объявлении метода указан оператор throws, т.е. обязанности по обработке данного исключению поручаются вызывающему методу. В данном случае вызывающим является метод main(), в котором и перехватывается исключение.
// Применение ключевого слова throws,
class ThrowsDemo {
// Обратите внимание на оператор throws в объявлении метода.
public static char prompt(String str)
throws java.io.IOException {
System.out.print(str + ": ");
return (char) System.in.read() ;
}
public static void main(String args[]) {
char ch;
try {
// В методе prompt() может быть сгенерировано исключение,
// поэтому данный метод следует вызывать в блоке try.
ch = prompt("Enter a letter");
}
catch(java.io.IOException exc) {
System.out.println("I/O exception occurred.");
ch = 'X';
}
System.out.println("You pressed " + ch);
}
}
Обратите внимание на одну особенность приведенного выше примера. Класс IOException относится к пакету java. io. Как будет разъяснено в главе 10, в этом пакете содержатся многие языковые средства Java для организации ввода-вывода. Следовательно, пакет java.io можно импортировать, а в программе указать только имя класса IOException.
Новые средства обработки исключений, внедренные в версии JDK 7
С появлением версии JDK 7 механизм обработки исключений в Java был значительно усовершенствован благодаря внедрению трех новых средств. Первое из них поддерживает автоматическое управление ресурсами, позволяющее автоматизировать процесс освобождения таких ресурсов, как файлы, когда они больше не нужны. В основу этого средства положена расширенная форма оператора try, называемая оператором try с ресурсами и описываемая в главе 10 при рассмотрении файлов. Второе новое средство называется многократным перехватом, а третье — окончательным или более точным повторным генерированием исключений. Два последних средства рассматриваются ниже.
Многократный перехват позволяет перехватывать два или более исключения одним оператором catch. Как пояснялось ранее, после оператора try можно (и даже принято) указывать два или более оператора catch. И хотя каждый блок оператора catch, как правило, содержит свою особую кодовую последовательность, нередко в двух или более блоках оператора catch выполняется одна и та же кодовая последовательность, несмотря на то, что в них перехватываются разные исключения. Вместо того чтобы перехватывать каждый тип исключения в отдельности, теперь можно воспользоваться единым блоком оператора catch для обработки исключений, не дублируя код.
Для организации многократного перехвата следует указать список исключений в одном операторе catch, разделив их типы оператором поразрядного ИЛИ. Каждый параметр многократного перехвата неявно указывается как final. (По желанию модификатор доступа final можно указать и явным образом, но это совсем не обязательно.) А поскольку каждый параметр многократного перехвата неявно указывается как final, то ему нельзя присвоить новое значение.
В приведенной ниже строке кода показывается, каким образом многократный перехват исключений ArithmeticException и ArraylndexOutOfBoundsException указывается в одном операторе catch.
catch(final ArithmeticException | ArraylndexOutOfBoundsException e) {
Ниже приведен краткий пример программы, демонстрирующий применение многократного перехвата исключений.
// Применение средства многократного перехвата исключений.
// Примечание: для компиляции этого кода требуется JDK 7
// или более поздняя версия данного комплекта,
class MultiCatch {
public static void main(String args[]) {
int a=88, b=0;
int result;
char chrs[] = { 'А', 'В', 'C' };
for(int i=0; i < 2; i++) {
try {
if (i == 0)
// сгенерировать исключение ArithmeticException
result = а / b;
else
// сгенерировать исключение ArraylndexOutOfBoundsException
chrs[5] = 'X';
}
// В этом операторе catch организуется перехват обоих исключений,
catch(ArithmeticException | ArraylndexOutOfBoundsException е) {
System.out.println("Exception caught: " + e);
}
}
System.out.println("After multi-catch.");
}
}
В данном примере программы исключение ArithmeticException генерируется при попытке деления на нуль, а исключение ArraylndexOutOfBoundsException — при попытке обращения за границы массива chrs. Оба исключения перехватываются одним оператором catch.
Средство более точного повторного генерирования исключений ограничивает этот процесс лишь теми проверяемыми типами исключений, которые генерируются в соответствующем блоке try и не обрабатываются в предыдущем блоке оператора catch, а также относятся к подтипу или супертипу указываемого параметра. И хотя такая возможность требуется нечасто, ничто не мешает теперь воспользоваться ею в полной мере. А для организации окончательного повторного генерирования исключений параметр оператора catch должен быть, по существу, указан как final. Это означает, что ему нельзя присвоить новое значение в блоке catch. Он может быть указан как final явным образом, хотя это и не обязательно.
Встроенные в Java исключения
В стандартном пакете java. lang определены некоторые классы, представляющие стандартные исключения Java. Часть из них использовалась в предыдущих примерах программ. Наиболее часто встречаются исключения из подклассов стандартного класса RuntimeException. А поскольку пакет java. lang импортируется по умолчанию во все программы на Java, то исключения, производные от класса RuntimeException, становятся доступными автоматически. Их даже обязательно включать в список оператора throws. В терминологии языка Java такие исключения называют непроверяемыми, поскольку компилятор не проверяет, обрабатываются или генерируются подобные исключения в методе. Непроверяемые исключения, определенные в пакете java.lang, приведены в табл. 9.2, тогда как в табл. 9.3 — те исключения из пакета j ava. lang, которые следует непременно включать в список оператора throws при объявлении метода, если, конечно, в методе содержатся операторы, способные генерировать эти исключения, а их обработка не предусмотрена в теле метода. Такие исключения принято называть проверяемыми. В Java предусмотрен также ряд других исключений, определения которых содержатся в различных библиотеках классов. К их числу можно отнести упоминавшееся ранее исключение IOException.
Таблица 9.2. Непроверяемые исключения, определенные в пакете java.lang
| Исключение | Описание |
|---|---|
| ArithmeticException | Арифметическая ошибка, например попытка деления на нуль |
| ArraylndexOutOfBoundsException | Попытка обращения за границы массива |
| ArrayStoreException | Попытка ввести в массив элемент, несовместимый с ним по типу |
| ClassCastException | Недопустимое приведение типов |
| EnumConstNotPresentException | Попытка использования нумерованного значения, которое не было определено ранее |
| IllegalArgumentException | Недопустимый параметр при вызове метода |
| IllegalMonitorStateException | Недопустимая операция контроля, например, ожидание разблокировки потока |
| IllegalStateException | Недопустимое состояние среды выполнения или приложения |
| IllegalThreadStateException | Запрашиваемая операция несовместима с текущим состоянием потока |
| IndexOutOfBoundsException | Недопустимое значение индекса |
| NegativeArraySizeException | Создание массива отрицательного размера |
| NullPointerException | Недопустимое использование пустой ссылки |
| NumberFormatException | Неверное преобразование символьной строки в число |
| SecurityException | Попытка нарушить систему защиты |
| StringlndexOutOfBounds | Попытка обращения к символьной строке за ее границами |
| TypeNotPresentException | Неизвестный тип |
| UnsupportedOperationException | Неподдерживаемая операция |
Таблица 9.3. Проверяемые исключения, определенные в пакете java.lang
| Исключение | Описание |
|---|---|
| ClassNotFoundException | Класс не найден |
| CloneNotSupportedException | Попытка клонирования объекта, не реализующего интерфейс Cloneable |
| IllegalAccessException | Доступ к классу запрещен |
| InstantiationException | Попытка создания объекта абстрактного класса или интер¬фейса |
| InterruptedException | Прерывание одного потока другим |
| NoSuchFieldException | Требуемое поле не существует |
| NoSuchMethodException | Требуемый метод не существует |
| ReflectiveOperationException | Суперкласс исключений, связанных с рефлексией (добавлен в версии JDK 7) |
Создание подклассов, производных от класса Exception
Несмотря на то что встроенные в Java исключения позволяют обрабатывать большинство ошибок, механизм обработки исключений не ограничивается только этими ошибками. В частности, можно создавать исключения для обработки потенциальных ошибок в прикладной программе. Создать исключение несложно. Для этого достаточно определить подкласс, производный от класса Exception, который, в свою очередь, является подклассом, порожденным классом Throwable. В создаваемый подкласс не обязательно включать реализацию каких-то методов. Сам факт существования такого подкласса позволяет использовать его в качестве исключения.
В классе Exception не определены новые методы. Он лишь наследует методы, предоставляемые классом Throwable. Таким образом, все исключения, включая и создаваемые вами, содержат методы класса Throwable. Конечно же, вы вольны переопределить в создаваемом вами классе один или несколько методов.
Ниже приведен пример, в котором создается исключение NonlntResultException. Оно генерируется в том случае, если результатом деления двух целых чисел является дробное число. В классе NonlntResultException содержатся два поля, предназначенные для хранения целых чисел, а также конструктор. В нем также переопределен метод toString(), что дает возможность выводить описание исключения с помощью метода println().
// Применение специально создаваемого исключения.
// создать исключение
class NonlntResultException extends Exception {
int n;
int d;
NonlntResultException(int i, int j) {
n = i;
d = j;
}
public String toString() {
return "Result of " + n + " / " + d +
" is non-integer.";
}
}
class CustomExceptDemo {
public static void main(String args[]) {
// В массиве numer содержатся нечетные числа,
int numer[] = { 4, 8, 15, 32, 64, 127, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
if((numer[i]%2) != 0)
throw new
NonlntResultException(numer[i], denom[i]);
System.out.println(numer[i] + " / " +
denom[i] + 11 is " +
numer[i]/denom[i]);
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can11 divide by Zero!");
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
}
catch (NonlntResultException exc) {
System.out.println(exc) ;
}
}
}
}
Результат выполнения данной программы выглядит следующим образом:
4 / 2 is 2
Can't divide by Zero!
Result of 15 / 4 is non-integer.
32 / 4 is 8
Can't divide by Zero!
Result of 127 / 8 is non-integer.
No matching element found.
No matching element found.
Пример для опробования 9.1.
Добавление исключений в класс очереди
В этом проекте предстоит создать два класса исключении, которые будут использоваться классом очереди, разработанным в примере для опробования 8.1. Эти исключения должны указывать на переполнение и опустошение очереди, а генерировать их будут методы put() и get() соответственно. Ради простоты эти исключения добавляются в класс FixedQueue, но вы можете без труда внедрить их в любые другие классы очереди, разработанные в примере для опробования 8.1.
Последовательность действий
- Создайте файл QExcDemo.java.
- Определите следующие исключения в файле QExcDemo.java:
/* Пример для опробования 9.1. Добавление обработчиков исключений в класс очереди. */ // Исключение, указывающее на переполнение очереди, class QueueFullException extends Exception { int size; QueueFullException(int s) { size = s; } public String toString() { return "nQueue is full. Maximum size is " + size; } } // Исключение, указывающее на опустошение очереди, class QueueEmptyException extends Exception { public String toString() { return "nQueue is empty."; } }Исключение QueueFullException генерируется при попытке поместить элемент в уже заполненную очередь, а исключение QueueEmptyException — в ответ на попытку извлечь элемент из пустой очереди.
- Измените класс FixedQueue таким образом, чтобы при возникновении ошибки он генерировал исключение. Соответствующий код приведен ниже. Введите этот код в файл QExcDemo.java.
// Класс, реализующий очередь фиксированного размера // для хранения символов. class FixedQueue implements ICharQ { private char q[]; // Массив для хранения элементов очереди, private int putloc, getloc; // Индексы размещения и извлечения // элементов очереди. // создать пустую очередь заданного размера public FixedQueue(int size) { q = new char[size+1]; // выделить память для очереди putloc = getloc = 0; } // поместить символ в очередь public void put(char ch) throws QueueFullException { if(putloc==q.length-1) throw new QueueFullException(q.length-1); putloc++; q[putloc] = ch; } // извлечь символ из очереди public char get() throws QueueEmptyException { if(getloc == putloc) throw new QueueEmptyException(); getloc++; return q[getloc]; } }Добавление исключений в класс FixedQueue выполняется в два этапа. Сначала в определении методов get() и put() указывается оператор throws с типом генерируемого исключения. А затем в этих методах организуется генерирование исключений при возникновении ошибок. Используя исключения, можно организовать обработку ошибок в вызывающей части программы наиболее рациональным способом. Как вы помните, в предыдущих версиях рассматриваемой здесь программы выводились только сообщения об ошибках. А генерирование исключений является более профессиональным подходом к разработке данной программы.
- Для опробования усовершенствованного класса FixedQueue введите в файл QExcDemo.java приведенный ниже исходный код класса QExcDemo.
// Демонстрация исключений при обращении с очередью, class QExcDemo { public static void main(String args[]) { FixedQueue q = new FixedQueue(10); char ch; int i; try { // Переполнение очереди. for(i=0; i < 11; i++) { System.out.print("Attempting to store : " + (char) ('A' + i)); q.put((char) (fA' + i)); System.out.println(" - OK"); } System.out.println(); } catch (QueueFullException exc) { System.out.println(exc); } System.out.println(); try { // Попытка извлечь символ из пустой очереди. for(i=0; i < 11; i++) { System.out.print("Getting next char: "); ch = q.get(); System.out.println(ch); } } catch (QueueEmptyException exc) { System.out.println(exc); } } } - Класс FixedQueue реализует интерфейс ICharQ, в котором определены методы get() и put(), и поэтому интерфейс ICharQ необходимо изменить таким образом, чтобы в нем отражалось наличие операторов throws. Ниже приведен видоизмененный соответственно код интерфейса ICharQ. Не забывайте о том, что он должен храниться в файле ICharQjava.
// Интерфейс очереди для хранения символов с генерированием исключений, public interface ICharQ { // поместить символ в очередь void put(char ch) throws QueueFullException; // извлечь символ из очереди char get() throws QueueEmptyException; } - Скомпилируйте сначала новую версию исходного файла IQChar. j ava, а затем исходный файл QExcDemo. java и запустите программу QExcDemo на выполнение. В итоге вы получите следующий результат ее выполнения:
Attempting to store A - OK Attempting to store В - OK Attempting to store С - OK Attempting to store D - OK Attempting to store E - OK Attempting to store F - OK Attempting to store G - OK Attempting to store H - OK Attempting to store I - OK Attempting to store J - OK Attempting to store К Queue is full. Maximum size is 10 Getting next char: A Getting next char: В Getting next char: С Getting next char: D Getting next char: E Getting next char: F Getting next char: G Getting next char: H Getting next char: I Getting next char: J Getting next char: Queue is empty.
Упражнение для самопроверки по материалу главы 9
- Какой класс находится на вершине иерархии исключений?
- Объясните вкратце, как пользоваться ключевыми словами try и catch?
- Какая ошибка допущена в приведенном ниже фрагменте кода?
// ... vals[18] = 10; catch (ArraylndexOutOfBoundsException exc) { // обработать ошибку } - Что произойдет, если исключение не будет перехвачено?
- Какая ошибка допущена в приведенном ниже фрагменте кода?
class A extends Exception { ... class В extends А { ... // ... try { // ... } catch (A exc) { ... } catch (В exc) { ... } - Может ли внутренний блок catch повторно генерировать исключение, которое будет обработано во внешнем блоке catch?
- Блок finally — последний фрагмент кода, выполняемый перед завершением программы. Верно или неверно? Обоснуйте свой ответ.
- Исключения какого типа необходимо явно объявлять с помощью оператора throws, включаемого в объявление метода?
- Какая ошибка допущена в приведенном ниже фрагменте кода?
class MyClass { // ... } // ... throw new MyClass(); - Отвечая на вопрос 3 упражнения для самопроверки по материалу главы 6, вы создали класс Stack. Добавьте в него специальные исключения для реагирования на попытку поместить элемент в переполненный стек и извлечь элемент из пустого стека.
- Какими тремя способами можно сгенерировать исключение?
- Назовите два подкласса, производных непосредственно от класса Throwable.
- Что такое многократный перехват?
- Следует ли перехватывать в программе исключения типа Error?
Содержание
- 1 Методы обработки ошибок
- 2 Исключения
- 3 Классификация исключений
- 3.1 Проверяемые исключения
- 3.2 Error
- 3.3 RuntimeException
- 4 Обработка исключений
- 4.1 try-catch-finally
- 4.2 Обработка исключений, вызвавших завершение потока
- 4.3 Информация об исключениях
- 5 Разработка исключений
- 6 Исключения в Java7
- 7 Примеры исключений
- 8 Гарантии безопасности
- 9 Источники
Методы обработки ошибок
1. Не обрабатывать.
2. Коды возврата. Основная идея — в случае ошибки возвращать специальное значение, которое не может быть корректным. Например, если в методе есть операция деления, то придется проверять делитель на равенство нулю. Также проверим корректность аргументов a и b:
Double f(Double a, Double b) {
if ((a == null) || (b == null)) {
return null;
}
//...
if (Math.abs(b) < EPS) {
return null;
} else {
return a / b;
}
}
При вызове метода необходимо проверить возвращаемое значение:
Double d = f(a, b); if (d != null) { //... } else { //... }
Минусом такого подхода является необходимость проверки возвращаемого значения каждый раз при вызове метода. Кроме того, не всегда возможно определить тип ошибки.
3.Использовать флаг ошибки: при возникновении ошибки устанавливать флаг в соответствующее значение:
boolean error; Double f(Double a, Double b) { if ((a == null) || (b == null)) { error = true; return null; } //... if (Math.abs(b) < EPS) { error = true; return b; } else { return a / b; } }
error = false; Double d = f(a, b); if (error) { //... } else { //... }
Минусы такого подхода аналогичны минусам использования кодов возврата.
4.Можно вызвать метод обработки ошибки и возвращать то, что вернет этот метод.
Double f(Double a, Double b) {
if ((a == null) || (b == null)) {
return nullPointer();
}
//...
if (Math.abs(b) < EPS) {
return divisionByZero();
} else {
return a / b;
}
}
Но в таком случае не всегда возможно проверить корректность результата вызова основного метода.
5.В случае ошибки просто закрыть программу.
if (Math.abs(b) < EPS) { System.exit(0); return this; }
Это приведет к потере данных, также невозможно понять, в каком месте возникла ошибка.
Исключения
В Java возможна обработка ошибок с помощью исключений:
Double f(Double a, Double b) {
if ((a == null) || (b == null)) {
throw new IllegalArgumentException("arguments of f() are null");
}
//...
return a / b;
}
Проверять b на равенство нулю уже нет необходимости, так как при делении на ноль метод бросит непроверяемое исключение ArithmeticException.
Исключения позволяют:
- разделить обработку ошибок и сам алгоритм;
- не загромождать код проверками возвращаемых значений;
- обрабатывать ошибки на верхних уровнях, если на текущем уровне не хватает данных для обработки. Например, при написании универсального метода чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей метод программы;
- классифицировать типы ошибок, обрабатывать похожие исключения одинаково, сопоставлять специфичным исключениям определенные обработчики.
Каждый раз, когда при выполнении программы происходит ошибка, создается объект-исключение, содержащий информацию об ошибке, включая её тип и состояние программы на момент возникновения ошибки.
После создания исключения среда выполнения пытается найти в стеке вызовов метод, который содержит код, обрабатывающий это исключение. Поиск начинается с метода, в котором произошла ошибка, и проходит через стек в обратном порядке вызова методов. Если не было найдено ни одного подходящего обработчика, выполнение программы завершается.
Таким образом, механизм обработки исключений содержит следующие операции:
- Создание объекта-исключения.
- Заполнение stack trace’а этого исключения.
- Stack unwinding (раскрутка стека) в поисках нужного обработчика.
Классификация исключений
Класс Java Throwable описывает все, что может быть брошено как исключение. Наследеники Throwable — Exception и Error — основные типы исключений. Также RuntimeException, унаследованный от Exception, является существенным классом.
![]()
Иерархия стандартных исключений
Проверяемые исключения
Наследники класса Exception (кроме наслеников RuntimeException) являются проверяемыми исключениями(checked exception). Как правило, это ошибки, возникшие по вине внешних обстоятельств или пользователя приложения – неправильно указали имя файла, например. Эти исключения должны обрабатываться в ходе работы программы, поэтому компилятор проверяет наличие обработчика или явного описания тех типов исключений, которые могут быть сгенерированы некоторым методом.
Все исключения, кроме классов Error и RuntimeException и их наследников, являются проверяемыми.
Error
Класс Error и его подклассы предназначены для системных ошибок. Свои собственные классы-наследники для Error писать (за очень редкими исключениями) не нужно. Как правило, это действительно фатальные ошибки, пытаться обработать которые довольно бессмысленно (например OutOfMemoryError).
RuntimeException
Эти исключения обычно возникают в результате ошибок программирования, такие как ошибки разработчика или неверное использование интерфейса приложения. Например, в случае выхода за границы массива метод бросит OutOfBoundsException. Такие ошибки могут быть в любом месте программы, поэтому компилятор не требует указывать runtime исключения в объявлении метода. Теоретически приложение может поймать это исключение, но разумнее исправить ошибку.
Обработка исключений
Чтобы сгенерировать исключение используется ключевое слово throw. Как и любой объект в Java, исключения создаются с помощью new.
if (t == null) { throw new NullPointerException("t = null"); }
Есть два стандартных конструктора для всех исключений: первый — конструктор по умолчанию, второй принимает строковый аргумент, поэтому можно поместить подходящую информацию в исключение.
Возможна ситуация, когда одно исключение становится причиной другого. Для этого существует механизм exception chaining. Практически у каждого класса исключения есть конструктор, принимающий в качестве параметра Throwable – причину исключительной ситуации. Если же такого конструктора нет, то у Throwable есть метод initCause(Throwable), который можно вызвать один раз, и передать ему исключение-причину.
Как и было сказано раньше, определение метода должно содержать список всех проверяемых исключений, которые метод может бросить. Также можно написать более общий класс, среди наследников которого есть эти исключения.
void f() throws InterruptedException, IOException { //...
try-catch-finally
Код, который может бросить исключения оборачивается в try-блок, после которого идут блоки catch и finally (Один из них может быть опущен).
try { // Код, который может сгенерировать исключение }
Сразу после блока проверки следуют обработчики исключений, которые объявляются ключевым словом catch.
try { // Код, который может сгенерировать исключение } catch(Type1 id1) { // Обработка исключения Type1 } catch(Type2 id2) { // Обработка исключения Type2 }
Сatch-блоки обрабатывают исключения, указанные в качестве аргумента. Тип аргумента должен быть классом, унаследованного от Throwable, или самим Throwable. Блок catch выполняется, если тип брошенного исключения является наследником типа аргумента и если это исключение не было обработано предыдущими блоками.
Код из блока finally выполнится в любом случае: при нормальном выходе из try, после обработки исключения или при выходе по команде return.
NB: Если JVM выйдет во время выполнения кода из try или catch, то finally-блок может не выполниться. Также, например, если поток выполняющий try или catch код остановлен, то блок finally может не выполниться, даже если приложение продолжает работать.
Блок finally удобен для закрытия файлов и освобождения любых других ресурсов. Код в блоке finally должен быть максимально простым. Если внутри блока finally будет брошено какое-либо исключение или просто встретится оператор return, брошенное в блоке try исключение (если таковое было брошено) будет забыто.
import java.io.IOException; public class ExceptionTest { public static void main(String[] args) { try { try { throw new Exception("a"); } finally { throw new IOException("b"); } } catch (IOException ex) { System.err.println(ex.getMessage()); } catch (Exception ex) { System.err.println(ex.getMessage()); } } }
После того, как было брошено первое исключение — new Exception("a") — будет выполнен блок finally, в котором будет брошено исключение new IOException("b"), именно оно будет поймано и обработано. Результатом его выполнения будет вывод в консоль b. Исходное исключение теряется.
Обработка исключений, вызвавших завершение потока
При использовании нескольких потоков бывают ситуации, когда поток завершается из-за исключения. Для того, чтобы определить с каким именно, начиная с версии Java 5 существует интерфейс Thread.UncaughtExceptionHandler. Его реализацию можно установить нужному потоку с помощью метода setUncaughtExceptionHandler. Можно также установить обработчик по умолчанию с помощью статического метода Thread.setDefaultUncaughtExceptionHandler.
Интерфейс Thread.UncaughtExceptionHandler имеет единственный метод uncaughtException(Thread t, Throwable e), в который передается экземпляр потока, завершившегося исключением, и экземпляр самого исключения. Когда поток завершается из-за непойманного исключения, JVM запрашивает у потока UncaughtExceptionHandler, используя метод Thread.getUncaughtExceptionHandler(), и вызвает метод обработчика – uncaughtException(Thread t, Throwable e). Все исключения, брошенные этим методом, игнорируются JVM.
Информация об исключениях
-
getMessage(). Этот метод возвращает строку, которая была первым параметром при создании исключения; -
getCause()возвращает исключение, которое стало причиной текущего исключения; -
printStackTrace()печатает stack trace, который содержит информацию, с помощью которой можно определить причину исключения и место, где оно было брошено.
Exception in thread "main" java.lang.IllegalStateException: A book has a null property
at com.example.myproject.Author.getBookIds(Author.java:38)
at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
at com.example.myproject.Book.getId(Book.java:22)
at com.example.myproject.Author.getBookIds(Author.java:35)
Все методы выводятся в обратном порядке вызовов. В примере исключение IllegalStateException было брошено в методе getBookIds, который был вызван в main. «Caused by» означает, что исключение NullPointerException является причиной IllegalStateException.
Разработка исключений
Чтобы определить собственное проверяемое исключение, необходимо создать наследника класса java.lang.Exception. Желательно, чтобы у исключения был конструкор, которому можно передать сообщение:
public class FooException extends Exception { public FooException() { super(); } public FooException(String message) { super(message); } public FooException(String message, Throwable cause) { super(message, cause); } public FooException(Throwable cause) { super(cause); } }
Исключения в Java7
- обработка нескольких типов исключений в одном
catch-блоке:
catch (IOException | SQLException ex) {...}
В таких случаях параметры неявно являются final, поэтому нельзя присвоить им другое значение в блоке catch.
Байт-код, сгенерированный компиляцией такого catch-блока будет короче, чем код нескольких catch-блоков.
-
Tryс ресурсами позволяет прямо вtry-блоке объявлять необходимые ресурсы, которые по завершению блока будут корректно закрыты (с помощью методаclose()). Любой объект реализующийjava.lang.AutoCloseableможет быть использован как ресурс.
static String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } }
В приведенном примере в качестве ресурса использутся объект класса BufferedReader, который будет закрыт вне зависимосити от того, как выполнится try-блок.
Можно объявлять несколько ресурсов, разделяя их точкой с запятой:
public static void viewTable(Connection con) throws SQLException { String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES"; try (Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query)) { //Work with Statement and ResultSet } catch (SQLException e) { e.printStackTrace; } }
Во время закрытия ресурсов тоже может быть брошено исключение. В try-with-resources добавленна возможность хранения «подавленных» исключений, и брошенное try-блоком исключение имеет больший приоритет, чем исключения получившиеся во время закрытия. Получить последние можно вызовом метода getSuppressed() от исключения брошенного try-блоком.
- Перебрасывание исключений с улучшенной проверкой соответствия типов.
Компилятор Java SE 7 тщательнее анализирует перебрасываемые исключения. Рассмотрим следующий пример:
static class FirstException extends Exception { } static class SecondException extends Exception { } public void rethrowException(String exceptionName) throws Exception { try { if ("First".equals(exceptionName)) { throw new FirstException(); } else { throw new SecondException(); } } catch (Exception ex) { throw e; } }
В примере try-блок может бросить либо FirstException, либо SecondException. В версиях до Java SE 7 невозможно указать эти исключения в декларации метода, потому что catch-блок перебрасывает исключение ex, тип которого — Exception.
В Java SE 7 вы можете указать, что метод rethrowException бросает только FirstException и SecondException. Компилятор определит, что исключение Exception ex могло возникнуть только в try-блоке, в котором может быть брошено FirstException или SecondException. Даже если тип параметра catch — Exception, компилятор определит, что это экземпляр либо FirstException, либо SecondException:
public void rethrowException(String exceptionName) throws FirstException, SecondException { try { // ... } catch (Exception e) { throw e; } }
Если FirstException и SecondException не являются наследниками Exception, то необходимо указать и Exception в объявлении метода.
Примеры исключений
- любая операция может бросить
VirtualMachineError. Как правило это происходит в результате системных сбоев. -
OutOfMemoryError. Приложение может бросить это исключение, если, например, не хватает места в куче, или не хватает памяти для того, чтобы создать стек нового потока. -
IllegalArgumentExceptionиспользуется для того, чтобы избежать передачи некорректных значений аргументов. Например:
public void f(Object a) { if (a == null) { throw new IllegalArgumentException("a must not be null"); } }
IllegalStateExceptionвозникает в результате некорректного состояния объекта. Например, использование объекта перед тем как он будет инициализирован.
Гарантии безопасности
При возникновении исключительной ситуации, состояния объектов и программы могут удовлетворять некоторым условиям, которые определяются различными типами гарантий безопасности:
- Отсутствие гарантий (no exceptional safety). Если было брошено исключение, то не гарантируется, что все ресурсы будут корректно закрыты и что объекты, методы которых бросили исключения, могут в дальнейшем использоваться. Пользователю придется пересоздавать все необходимые объекты и он не может быть уверен в том, что может переиспозовать те же самые ресурсы.
- Отсутствие утечек (no-leak guarantee). Объект, даже если какой-нибудь его метод бросает исключение, освобождает все ресурсы или предоставляет способ сделать это.
- Слабые гарантии (weak exceptional safety). Если объект бросил исключение, то он находится в корректном состоянии, и все инварианты сохранены. Рассмотрим пример:
class Interval { //invariant: left <= right double left; double right; //... }
Если будет брошено исключение в этом классе, то тогда гарантируется, что ивариант «левая граница интервала меньше правой» сохранится, но значения left и right могли измениться.
- Сильные гарантии (strong exceptional safety). Если при выполнении операции возникает исключение, то это не должно оказать какого-либо влияния на состояние приложения. Состояние объектов должно быть таким же как и до вызовов методов.
- Гарантия отсутствия исключений (no throw guarantee). Ни при каких обстоятельствах метод не должен генерировать исключения. В Java это невозможно, например, из-за того, что
VirtualMachineErrorможет произойти в любом месте, и это никак не зависит от кода. Кроме того, эту гарантию практически невозможно обеспечить в общем случае.
Источники
- Обработка ошибок и исключения — Сайт Георгия Корнеева
- Лекция Георгия Корнеева — Лекториум
- The Java Tutorials. Lesson: Exceptions
- Обработка исключений — Википедия
- Throwable (Java Platform SE 7 ) — Oracle Documentation
- try/catch/finally и исключения — www.skipy.ru