How does the error handling work in Kotlin

Error handling in Kotlin / Java: How does it work correctly?


A source


Error handling is essential in any development. Almost everything in the program can go wrong: the user enters wrong data or they can come like this over http, or we made a mistake while writing the serialization / deserialization and while processing the program crashes with an error. Yes, the storage space can be mundane.


spoiler

¯_ (ツ) _ / ¯, there is no single way, and in any specific situation you need to choose the most appropriate option, but there are recommendations on how you can do better.


Preface


Unfortunately (or is it just a life like that?) The list is endless. The developer has to constantly think about the possibility of a bug somewhere and there are two situations:


  • when an expected error occurs while calling a function that we have provided and can try to handle;
  • if an unexpected error occurs during work that we did not anticipate.

And if the expected errors are at least localized, the rest can happen almost anywhere. If we don't handle anything important, we can simply crash with an error (although this behavior is insufficient and the minimum requirement is to add a message to the error log). But what if the payment is being processed and you can't just drop but at least have to return a response to an unsuccessful operation?


Before we get into how to deal with errors, a few words about exceptions:


exception



A source


The exception hierarchy is well described and you can find a lot of information about it. So it doesn't make sense to describe them here. What is sometimes still hotly debated is and error. And although the majority accepted exceptions as preferred (there are no exceptions in Kotlin), not everyone agrees yet.


Pro It was really a good intention to make it a convenient mechanism for handling errors, but reality made its own adjustments, although the idea of ​​including any exceptions that could be thrown into the signature from this feature in introduce the signature that is understandable and logical.


Let's look at this with an example. Suppose we have a function that can throw a testable exception ... Such a function would look like this:



It can be seen from its description that it can throw one exception and that there can only be one exception. Looks very comfortable? And as long as we have a small program, everything is like that. However, if the program is a bit bigger and there are more such functions, then some problems arise.


Checked exceptions require, according to the specification, that the function signature all possible Lists verifiable exceptions (or a common ancestor for them). Hence, if we have a call chain -> -> and the most nested function throws some kind of exception, then it needs to be set for everyone in the chain. And if there are more than one of these exceptions, the top function in the signature should contain a description of all of them.


As the program becomes more complex, this approach causes the exclusions of the functions above to gradually collapse into common ancestors and ultimately reduce to ... As in this form, becomes exception and negates all benefits of checked exceptions.


And when we take into account that a program, like a living organism, is constantly changing and evolving, it is practically impossible to predict in advance what exceptions may appear in it. As a result, it turns out that when we add a new function with a new exception, we have to go through the entire chain of its use and change the signatures of all the functions. Agree, this isn't the most pleasant experience (even considering modern IDEs do it for us).


But the final and probably biggest nail in checked exceptions "drove" lambdas out of Java 8. There are no checked exceptions ¯_ (ツ) _ / ¯ in their signature (since any function in a lambda can be called with any signature), so every function call with an activated exception from the Lambda forces it to be included as deactivated in an exception case:



Fortunately, there are no checked exceptions at all in the JVM specification. So in Kotlin you can't wrap anything in the same lambda, just call the function you want.


although sometimes ...

This sometimes leads to unexpected consequences, such as incorrect work in what is only "expected" exceptions. However, this is more of a feature of the framework, and this behavior may change in the spring in the near future.


Exceptions themselves are special objects. In addition to being "routed" through methods, they also collect batch traces as they are created. This feature then helps analyze problems and locate errors, but it can also lead to performance problems if the application logic is heavily tied to thrown exceptions. As shown in the article, disabling the stack trace assembly in this case can significantly improve performance. However, it should only be used in exceptional cases if this is really necessary!


Error processing


The main thing with "unexpected" errors is to find a place to catch them. In JVM languages, this can either be a stream creation point or a filter / entry point for an http method where you can try catch processing errors. If you are using a framework, most likely it can already write common error handlers, since for example you can use annotated methods in the Spring Framework ...


At the same central processing points, you can "throw" exceptions that we do not want to handle in certain places, and the same throw exceptions (for example, when we do not know what to do in a particular place and how to deal with a bug). However, this method is not always appropriate because you sometimes need to troubleshoot an error in place and verify that all places of function calls are handled correctly. Let's look at ways to do this.


  1. Keep using exceptions and the same try-catch:

    The main disadvantage is that we can "forget" to try-catch it at the calling point and skip trying to handle the exception directly, which throws the exception all the way to the general error-handling point. Here you can go exceptions (for Java), but then we get all of the disadvantages mentioned above. This approach is useful when direct error handling is not always required but is rarely required.
  2. Use a sealed class as a result of a (Kotlin) call.
    In Kotlin, you can limit the number of heirs of a class and make them predictable at compile time. This allows the compiler to verify that all possible options in the code are parsed. In Java, you can create a common interface and multiple inheritance without losing compilation checks.

    Here we get something like approach to errors, when you need to explicitly check (or explicitly ignore) the resulting values. The approach is very handy and especially useful when you have a lot of parameters to enter in any situation. Class can be extended with various methods that make it easier to get the result by throwing the above exception (if any) (that is, we don't need to handle the error at the place of the call). The main disadvantage is just the creation of unnecessary intermediate objects (and a slightly more verbose notation), but it can also be removed using classes (if one argument is enough for us). and as a private example there is a class of Kotlin. It's true, it's still for internal use only because the implementation may change slightly in the future. However, if you want to use them, you can add a compilation flag ...
  3. As one of the possible types of point 2, the use of a type from functional programming. This can either be a result or an error. The guy himself can be like class and class. Below is an example of using the implementation from the library:

    Most suitable for those who like a functional approach and like to build call chains.
  4. Use or type from Kotlin:

    This approach is appropriate when the cause of the failure is not very important and when there is only one. An empty answer is considered an error and will be forwarded above. The shortest data set without creating additional objects, but this approach cannot always be used.
  5. It is similar to point 4, but only uses a hardcode value as an error marker:

    This is probably the oldest approach to error handling (or even with Algol). There is no overhead, just incomprehensible code (along with restrictions on the choice of result), but contrary to paragraph 4, different error codes can be generated if more than one possible exception is required.

Results


All approaches can be combined depending on the situation and there is none that is suitable in all cases.


For example, you can take the approach to bugs with classes, and where it's not very convenient, go to bugs.


Or in most places use -type as a marker that it was not possible to calculate the value or get it from somewhere (e.g. as an indicator that the value was not found in the database).


And if you have fully working code along with or some other similar library then it is most likely better to use ...


With the http servers it is easiest to report all errors to the central points and only to combine an approach with classes in a few places.


I would love to see which of these you use in the comments, or maybe there are other convenient ways to troubleshoot errors?


And thanks to everyone who read to the end!