
I have never had any particular opinion regarding error handling. If I started working with existing code, I continued to perform the task on which the author of the source worked; if I wrote code from scratch, I did what seemed correct to me.
But recently I ran into a problem, a bug that manifested itself due to a "silent" error in the code. I realized that there is something to think about. Maybe I can’t change the way that errors are handled in the entire code base I’m working on, but something can definitely be optimized.
We remind: for all readers of "Habr" - a discount of 10,000 rubles when recording for any Skillbox course on the promotional code "Habr".
')
Skillbox recommends: Online Profession Java Developer Educational Course.
Not always worth chopping off the shoulder
The first step in the issue of error handling should be to understand when “error” is not “error!”. Of course, it all depends on the business logic of your application, but in general, some bugs are obvious and can be fixed without problems.
- Do you have a date range where the “before” is before the “from”? Change the order.
- Do you have a phone number that starts with + or contains a dash where you do not expect special characters to appear? Remove them.
- Null collection - a problem? Make sure you initialize this before accessing (using lazy initialization or a constructor).
Do not interrupt the execution of the code due to errors that you can correct, and, of course, do not interfere with the actions of users of your service or application. If you are able to understand the problem and solve it on the fly - just do it.

Return Null or other magic numbers
Zero values, –1 where a positive number is expected, and other magic return values ​​— all this is caused by the devil, which transfers the responsibility for error checking to the calling function.
With them, your code will be full of such blocks that will make the application logic unclear:
return_value = possibly_return_a_magic_value() if return_value < 0: handle_error() else: do_something() other_return_value = possibly_nullable_value() if other_return_value is None: handle_null_value() else: do_some_other_thing()
Well, or simpler, but also bad:
var item = returnSomethingWhichCouldBeNull(); var result = item?.Property?.MaybeExists; if (result.HasValue) { DoSomething(); }
Passing null into methods is also a problem, although in some developers you may find places where the methods start with a few lines of input validation. But in fact, all this is not necessary. Most modern languages ​​provide not one, but several tools at once, allowing you to clearly indicate what you expect. They also skip useless checks, for example, defining parameters as non-zero or having an appropriate decorator.
Error codes
Here is the same problem as with null and other similar values, when an unnecessary complication of code is added.
For example, you can use this structure to return an error code:
int errorCode; var result = getSomething(out errorCode); if (errorCode != 0) { doSomethingWithResult(result); }
The result can be displayed as follows:
public class Result<T> { public T Item { get; set; }
This is really not the best code. It turns out that Item can still be empty. In fact, there is no guarantee (other than an agreement) that when the result is error free, you can safely access the Item.
After you finish all this processing, you still have to convert the bug code into an error message and do something with it. Sometimes it happens that because of the excessive complexity of the code, this message itself is not displayed as it should.
There is an even more serious problem: if you or someone else changes the internal implementation to handle the new invalid state with the new error code, the whole structure will stop working altogether.
If it didn't work out the first time, try again.
Before proceeding, it is worth emphasizing that the “silent” failure of the program is bad. An unexpected problem may arise at the most inopportune moment - for example, at the weekend, when you cannot quickly fix it.

If you read
Clean Code , then you are most likely wondering, why not just “throw” an exception? If not, then most likely you think that exceptions are the root of evil. I used to think so too, but now I think a little differently.
An interesting, at least for me, nuance is that the default implementation for a new method in C # is to create a NotImplementedException exception, whereas for the new method in Python, the default is “pass”.
As a result, most often we introduce the “silent error” setting for Python. I wonder how many developers have spent a lot of time in order to understand what is happening and why the program does not work. But in the end, after many hours, they found that they forgot to implement the placeholder method.
But take a look at this:
public MyDataObject UpdateSomething(MyDataObject toUpdate) { if (_dbConnection == null) { throw new DbConnectionError(); } try { var newVersion = _dbConnection.Update(toUpdate); if (newVersion == null) { return null; } MyDataObject result = new MyDataObject(newVersion); return result; } catch (DbConnectionClosedException dbcc) { throw new DbConnectionError(); } catch (MyDataObjectUnhappyException dou) { throw new MalformedDataException(); } catch (Exception ex) { throw new UnknownErrorException(); } }
Of course, exceptions alone will not protect you from creating unreadable and unmanaged code. You must apply exceptions as a well-thought-out and well-considered strategy. Otherwise, if the project is too large, your application may be in an inconsistent state. Conversely, if the project is too small, you will get chaos.
To prevent this from happening, I advise you to keep in mind this:
Consistency above all else You should always make sure that the application is consistent. If this means that you have to wrap each pair of lines with a try / catch block, just hide it all in another function.
def my_function(): try: do_this() do_that() except: something_bad_happened() finally: cleanup_resource()
Error consolidation is required. Well, if you provide different types of processing of different types of errors. However, you need it, not the users. For them, display the only exception, so that users simply know that something went wrong. Details - your area of ​​responsibility.
public MyDataObject UpdateSomething(MyDataObject toUpdate) { try { var newVersion = _dbConnection.Update(toUpdate); MyDataObject result = new MyDataObject(newVersion); return result; } catch (DbConnectionClosedException dbcc) { HandleDbConnectionClosed(); throw new UpdateMyDataObjectException(); } catch (MyDataObjectUnhappyException dou) { RollbackVersion(); throw new UpdateMyDataObjectException(); } catch (Exception ex) { throw new UpdateMyDataObjectException(); } }
It is worth catching exceptions immediately. It is better to intercept them at the lowest level. Maintain consistency and hide the details as described above. Error handling should be left to the top level of the application. If everything is done correctly, you will be able to separate the flow of the logic of the application itself from the error handling flow. This will allow you to write clear, readable code.
def my_api(): try: item = get_something_from_the_db() new_version = do_something_to_item(item) return new_version except Exception as ex: handle_high_level_exception(ex)
So far this is all, and if you want to discuss the topic of errors and their processing, we will.
Skillbox recommends: