Where is the mistake, Billy? We need a mistake ...







Some time ago, my colleague published an article about error handling in Java / Kotlin. And it became interesting to me what methods of error transmission exist in programming in general. If you are also interested, then under the cut is the result of research. Most likely, some exotic methods have been omitted, but here there is only one hope for comments, which are sometimes more interesting and useful on the Habré article. :)



In the history of programming languages, not many ways have been invented to convey an error. If we completely disengage, there are only three of them: a direct return from a function, a transfer of control, and setting the state. Everything else is, to one degree or another, a mixture of these approaches. Below I tried to collect and describe the main representatives of these three species.



Disclaimer: for brevity and simplification of perception for any isolated executable code that generates an error, I will use the word “function”, and for any non-primitive (integer, string, boolean, etc ...) entities - “structure”.



Direct return



Direct return is simple. Although this is probably the most commonly used method, but there are many options. The method of processing combines them all - comparing the return value with predefined values.



  1. Return run status. The most banal option is TRUE (if it was executed normally) or FALSE (if there was a failure).
  2. Return the correct value in case of success and incorrect in case of error.

    C / c ++

    The strchr () function returns a pointer to the first occurrence of the character ch in the string pointed to by str. If ch is not found, NULL is returned.


    Quite often, approaches 1 and 2 are used in conjunction with setting the state.

  3. Return error code. If we want to not only know that the execution ended incorrectly, but also to understand where the error occurred in the function. Usually, if the function completed without error, the code 0 is returned. In the case of an error, the code is used to determine a specific place in the body of the function where something went wrong. But this is not an iron rule, look, for example, at HTTP with its 200.
  4. Return error code in an invalid range of values. For example, normally a function should return a positive integer, and in case of an error, its code with a minus sign.



    function countElements(param) { if (!isArray(param)) { return -10; } else if(!isInitialized(param)){ return -20 } else { return count(array); } }
          
          





  5. Return different types for positive and negative results. For example, nominally - a string, but not nominally - a number or class Success and class Error .



     sealed class UserProfileResult { data class Success(val userProfile: UserProfileDTO) : UserProfileResult() data class Error(val message: String, val cause: Exception? = null) : UserProfileResult() } val avatarUrl = when (val result = client.requestUserProfile(userId)) { is UserProfileResult.Success -> result.userProfile.avatarUrl is UserProfileResult.Error -> "http://domain.com/defaultAvatar.png" }
          
          





    You can also recall Either from the world of functional programming. Although here you can argue.

  6. Returning a structure containing both the result itself and the error.



     function doSomething(): array { ... if($somethingWrong === true) { return ["result" => null, "error" => "Alarm!!!"]; } else { return ["result" => $result, "error" => null]; } ... }
          
          





  7. Return multiple values. At first I was inclined not to separate this method from the previous one, but in the end I decided to put it in a separate paragraph. This option is quite rare, because it can be used exclusively in languages ​​that allow you to return several values ​​from a function, and there aren’t many of them. The most striking, but not the only example is the Go language.



     f, err := Sqrt(-1) if err != nil { fmt.Println(err) }
          
          





Setting State



The oldest and hardcore version, which has not lost its relevance to this day. It consists in the fact that the function does not return anything, and in case of an error it writes its value (in any form) to a separate entity, whether it is a processor register, a global variable or a private class field. To handle this kind of error, you need to independently extract the value from the right place and check it.



  1. Setting the "global" state. I took it in quotes because most often we are talking about globality in a certain scope.



     # ls /unknown/path 2>/dev/null # echo $? 1
          
          





  2. Setting your own state. When we have some structure providing a function. The function sets the state for this structure, and the error is already extracted from the structure either directly or using another specialized function.



     $mysqli = new mysqli("localhost", "my_user", "my_password", "world"); $result = $mysqli->query("SET a=1"); if ($mysqli->errno) { printf(" : %d\n", $mysqli->errno); }
          
          





  3. Setting the state of the returned object. Strongly echoes with paragraph 6. from the previous section. Unlike the previous paragraph, the status check is performed on the returned structure, and not on the one providing the function. As an obvious example, you can use the HTTP protocol and countless libraries in a variety of languages ​​that work with it.



     Response response = client.newCall("https://www.google.com").execute(); Integer errorCode = response.getCode();
          
          







Control transfer



And now we come to the most fashionable paradigm. Exceptions, callbacks, global error handlers - all this. What unites them all is that in the event of an error, control is transferred to a predetermined handler, and not to the code that called the function.



  1. Exceptions Everyone knows throw / try / catch. Throwing an exception, the function forms a structure that describes the error and, most often, contains various useful metadata that facilitate the diagnosis of the problem (for example, the call stack). After that, this structure is passed to a special mechanism that "rolls back" along the call stack to the first try block, which is associated with catch, which can handle exceptions of this type. This method is good in that the entire logic of throwing an exception is implemented by the runtime itself. The same is bad, since overhead costs (let's only without holivarov :)).
  2. Global error handlers. Not the most common way, but it does. I don’t even know what to tell here. Is it possible to note that the mechanisms of browsers can also be attributed here: when the code working away from the main stream monitors the events arriving in it.



     function myErrorHandler($errno, $errstr, $errfile, $errline) { echo "<b>Custom error:</b> [$errno] $errstr<br>"; echo " Error on line $errline in $errfile<br>"; } set_error_handler("myErrorHandler");
          
          





  3. Callbacks. They are dearly loved by developers for Android, JavaScript and apologists for reactive programming. The essence is simple: in addition to the processed data, handler functions are transferred to the function. In case of an error, the main function will call the corresponding handler and pass the error to it.



     var observer = Rx.Observer.create( x => console.log(`onNext: ${x}`), e => console.log(`onError: ${e}`), () => console.log('onCompleted'));
          
          







It seems to have forgotten nothing.



And a funny fact. Probably the most original way to return an error, combining at the same time exceptions, setting the state and returning several values, I met in Informix SPL (I write from memory):



 CREATE PROCEDURE some_proc(...) RETURNING int, int, int, int; … ON EXCEPTION SET SQLERR, ISAMERR RETURN 0, SQLERR, ISAMERR, USRERR; END EXCEPTION; LET USRERR = 1; -- do Something That May Raise Exception LET USRERR = 2; -- do Something Other That May Raise Exception … RETURN result, 0, 0, 0; END PROCEDURE
      
      






All Articles