How to handle errors correctly: silence is not always good
- Transfer
- Recovery mode

I have never had any particular opinion regarding error handling. If I started working with existing code, I continued to perform the task that the author of the source worked on; if I wrote the code from scratch, I did what seemed right 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 reflect on. Perhaps I can’t change the way I handle errors in the entire code base I'm working on, but something can definitely be optimized.
We remind you: for all readers of “Habr” - a discount of 10,000 rubles when registering for any Skillbox course using the “Habr” promo code.
Skillbox recommends: The on-line educational course "Profession Java-developer" .
It's not always worth chopping off the shoulder
The first step in error handling should be understanding when “error” is not “error!” Of course, all this depends on the business logic of your application, but in general, some bugs are explicit and can be fixed without problems.
- Do you have a date range where the “before” is before the “from”? Reorder.
- 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 problem? Make sure you initialize this before access (using lazy initialization or the constructor).
Do not interrupt code execution due to errors that you can fix, and, of course, do not interfere with your actions to 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 the product of the devil, which transfers responsibility for checking errors 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 to methods is also a problem, although in some developers' code you may find places where methods start with several lines of input validation. But in fact, all this is not necessary. Most modern languages provide not one, but several tools at once, which allow you to clearly indicate what you expect. They also skip useless checks, for example, defining parameters as nonzero or with the corresponding decorator.
Error Codes
This is the same problem as with null and other similar values when unnecessary code complication is added.
For example, you can use this construction 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
{
public T Item { get; set; }
// At least "ErrorCode" is an enum
public ErrorCode ErrorCode { get; set; } = ErrorCode.None;
public IsError { get { return ErrorCode != ErrorCode.None; } }
}
public class UsingResultConstruct
{
...
var result = GetResult();
if (result.IsError)
{
switch (result.ErrorCode)
{
case ErrorCode.NetworkError:
HandleNetworkError();
break;
case ErrorCode.UserError:
HandleUserError();
break;
default:
HandleUnknownError();
break;
}
}
ActuallyDoSomethingWithResult(result);
...
} 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 does not contain an error, you can safely access the Item.
After you finish with all this processing, you still have to convert the bug code into an error message and do something with it. Sometimes it happens that, due to 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 a new invalid state with a new error code, then the whole structure will stop working at all.
If it didn’t work the first time, try again
Before continuing, it is worth emphasizing that a “silent” program failure is bad. An unexpected problem can occur at the most inopportune moment - for example, on the weekend, when you cannot quickly fix everything.

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 point, at least for me, is that the default implementation for the new method in C # is to raise a NotImplementedException, while the default for the new method in Python is “pass”.But take a look at this:
As a result, most often we enter the “silent error” setting for Python. I wonder how many developers 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 discovered that they forgot to implement the placeholder method.
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 should apply exceptions as a well thought out and well-balanced 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 this in mind:
Consistency is above all. You must always make sure that the application is consistent. If this means that you have to wrap every couple 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()Consolidation of errors is necessary. It’s good if you provide different types of handling different types of errors. However, this is for you, not users. For them, throw the only exception so that users simply know that something went wrong. Details are 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 right away. They are best intercepted at the lowest level. Keeping consistent and hiding the details is worth it as described above. Error handling should be left to the top level of the application. If everything is done correctly, you can separate the logic flow of the application itself from the error processing 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)That's all for now, and if you want to discuss the topic of errors and their processing - Wellcome.
Skillbox recommends:
- Practical course "Mobile Developer PRO" .
- Online course "C # developer from scratch . "
- Two-year practical course "I am a PRO web developer . "