Improving your Swift code quality using error handling mechanism
Why should you keep track of code quality
Once you are becoming a part of the development team, you take a list of responsibilities on your shoulders. Besides such significant activities as participating in daily meetings, consulting colleagues and drinking coffee, you should ensure your code to be readable, maintainable and clean, so all the teammates can further understand its idea, even when you are unreachable and there is no option to find it out from you.
Common proverb “Treat people in the way you want you be treated” in software development can be interpreted as “Write your code in the way you want other developers to write their code”.
That it is a tremendously rare case when the same developer is being a project part from the very beginning till the complete end of it. Circumstances are changing, programmers are getting offers from other customers, becoming non-code managers and relocating to different countries, so any kind of possibility to be assisted with understanding their code logic is expiring. Therefore, as a software engineer, you have to quit using a paradigm “If someone has a hard time analyzing my code, he/she will ask me”.
The topic of code standards and transparency is so colossal, that it is possible to write the whole book about it, but, as the title states, today we are reviewing how Swift standard error handling tools can help to improve your code quality, so, let’s dive directly in this concept.
Quick Swift error handling functional overview
In order to work with errors in Swift, you should create an enumeration which represents all the possible failure cases. Generating enumerations is actually optional, but it is much more convenient to handle errors like that. It is obligatory that you make that enum an Error protocol compliant, since this is the only way how Swift can realize that its case can be thrown as an error.
As the functional element which is to implement error creating you should define a throwing method using the keyword “throws” in its declaration. Function can be of any type you need it to be.
As you can see above, function can either throw an error specified in PotentialFailureCases enum if something went wrong, or return true in case everything worked out fine. In order to further get this function called, you will need to construct do-try-catch block.
As “somethingWentWrong” in “potentiallyReturningErrorFunction()” is just a variable of boolean type with either true or false can be assigned to it, “true” will be printed if variable is true and “errorOne” will be printed if variable is false.
The logic of the above block is quite simple: you do the block in which you try the function and catch an error if function throws it. Obviously, since the function can alternatively return Bool value, you are able to handle it in “do” block.
A real life example
Market segment occupied by offline-only apps is extremely small, so, as an iOS developer, you should repeatedly deal with making network requests and handling their responses. As some server comebacks are not guaranteed to be successful, proper fail case processing strategy is a must-have in your system.
Imagine yourself working on relatively slight US located shopping app using a VIPER architecture. Your presenter gets raw information from interactor, formats it into an actual view model and passes it to view layer among with a command to use it for setting interface up. Nothing extraordinary at all. You are developing screen for confirming orders with only two buttons on it, one for confirming order, another one for being redirected back to main screen. So, you have only one potential network request your module can arrange (for the sake of simplicity functionality is minimized). Once you are confirming your order, interactor is making such a request:
http://yourWebsite?session=DVJDSNV94JNFJNV9322KKMKDS&query=ConfirmOrder&item_id=48312
After this request is completed, your server is returning JSON which is being parsed to the model below:
If everything works out fine on the server side, you are getting 0 status code and and “Success” status description, if something went wrong, the server sends 1 and “Something went wrong” message.
So, your Presenter function for handling network response looks like that:
Under the circumstances described above it works perfectly fine, but, as it usually happens, after the first release all rush is behind and it is time for everybody to analyze their performance and improve current functionality. Taking this into consideration, your backend decides to modernize existing API and insert error description to their responses. From now, success scenario remains the same, but there is a flexibility in a failure status codes scheme: 1 = “Invalid session”, 2 = “Invalid item ID”, 3 = “Query not found”.
For sure, you can leave everything like you have now, but, as we all understand, that is much better to know what exactly happened if failure occurs. How would you handle that? Let me guess…switch?
Nice! Now if any of errors occurs, we will see its description in debugger. But, as any inflexible decision, it works well only by the certain time. Business expansion always leads to its services growth. So, after a successful local market conquering, your client decides to perform international shipping. Therefore, now you have to improve your network request so it contains more parameters:
http://yourWebsite?session=DVJDSNV94JNFJNV9322KKMKDS&query=ConfirmOrder&item_id=48312&receiving_country=Ukraine&expectation_date=10/10/2021
Making a local order is achieved by passing the url with initial parameters.
Now user can select a country to ship the item to and date when he/she expects the order to arrive. Possible error codes are now supplemented by 100 = “Shipping to this country is not supported” and 101 = “It is impossible for the order to arrive by \(expectation_date)”. Because of that customer requires you to react on added error codes and show modal screen to select another country or prolong expectation date. Flow after receiving 1,2,3 codes remain the same. Let’s try:
Well, that is still not too bad, honestly. After some time being successful on an international market, customer decides to establish own internal logistics instead of using a 3rd party commercial delivery, so he needs the request to contain State and parameter indicating if a client would like a courier service. In the sake of sorting and handling simplicity, backend decides to separate ConfirmOrder request to two independent queries, so from now you are working with two requests:
http://yourWebsite?session=DVJDSNV94JNFJNV9322KKMKDS&query=ConfirmLocalOrder&item_id=48312&receiving_state=CA&expectation_date=09/30/2021&courier=true
in case of local order and
http://yourWebsite?session=DVJDSNV94JNFJNV9322KKMKDS&query=ConfirmInternationalOrder&item_id=48312&receiving_country=Ukraine&expectation_date=10/10/2021
in case of international order. Error codes 1,2 and 3 remain the same for both requests, but 1** codes are now different. ConfirmLocalOrder has only 100 code stating that “There is no courier service available in that region”. If such error occurs, you need to show the user an alert to confirm that he can pick the order by his own at the post office. ConfirmInternationalOrder has the same 100 and 101 error codes as before for international shipping.
Now you have two ways how you can handle that issue. First is to create a single function for handling errors and pass sender to it, so correct behaviour is achieved:
Function above will be called two times, first time from function which is responsible for handling ConfirmLocalOrder service response, and second time from ConfirmInternationalOrder service response handler method. Let’s be completely honest with each other, nesting switch in switch does not look cool at all, especially knowing that this function has a growing potential with more error codes coming.
As an alternative, you could also create numerous functions for handling each possible error scenario in class:
DRY principle has left the chat.
As you realize, none of above suits us if we follow software development principles and guidelines. Additionally, the code in both cases is not really readable due to senseless numbers as switch conditions.
I know the motivation took forever, but I had to show to what can the wrong error handling tools utilizing lead.
Finally let’s look how it can be optimized
In order to avoid a trap we’ve got into, another error handling approach should be used from the very beginning of class evolution.
Quick reminder, we are using this model for network response parsing:
First and foremost, let’s describe all the possible error cases for both requests using enums with raw values as server error codes themselves. Amount of enums should be equal to amount of network responses being analyzed in a class.
Let’s now create a function for handling response our server will return in case of passing either ConfirmInternationalOrder or ConfirmLocalOrder. We also declare a RequestType enum, so a category of response can be passed further.
In simple words, we try to handle response from one of the services for confirming order and, if it was not successful, we handle an error produced by it. If the error is not thrown by status code handler function, we just miss the “catch” block and ask view to show alert that order has been confirmed. Functional for handling status code and potential error is reflected here:
As we see, function for handling response status code checks whether it is equal to zero and, if not, throws a custom error which is initialized as an error enum case matching the status code. After the error is thrown, it is being caught and handled by error handling function through casting the general Error protocol type instance into each possible error in class scope and further performing the action basing on the specific error type.
By getting all this transformation done, we did increase readability of our code, since any fresh developer, having in the beginning no clue what can be caused by each network error, will now have a solid clarification of how the error is described and what is the possible negative outcome of it. We did also simplified further extensibility of our class, since any changes which are to be made in order to handle more errors, will be accomplished in the same places by performing similar actions.
You can play with this functional how ever you want, creating a separate error handler class and integrating it into every network-correlated module would be a great pick.