A note on exceptions. Exceptions have a bad reputation in some quarters. They're not in Go, Rust, or C, and they're troublesome in Java and C++. They're successful in Python. Why is this?
It took several go-rounds to make Python exceptions generally useful. Originally, exceptions were just strings. Exceptions became more useful when an standard hierarchy of exceptions was defined and libraries were modified to use it. The Python exception hierarchy has a root of Exception, and one of its subclasses is EnvironmentError. Errors that reflect problems external to the program, such as I/O, network, and external data format errors, are subclasses of EnvironmentError. So if you catch EnvironmentError and report it properly, that covers most common cases. Exceptions not under EnvironmentError generally indicate program bugs, so those are usually allowed to propagate upward to the point where crash-level errors are handled. The default is a program abort and a stack backtrace.
The other problem is getting things closed out properly after an exception. C++ tried doing this by having destructors close things. This works until something goes wrong in a destructor. Exceptions raised within destructors tend to cause problems. This tends to result in ignoring errors at close time. Go's "defer" has the same problem. Calling destructors from a garbage collector is even worse. Microsoft tried to address this in Microsoft Managed C++, resulting in painfully complicated destructor semantics. (Look up "re-animation" for Managed C++, if you like.)
The Python solution was a "with" clause. This is borrowed from Common LISP's "(with-open-file ..) construct. The Python syntax is "with opening expression as object handle :". The opened object is open for the scope of the with clause, and upon exit from the with clause, the "__exit__" method of the object will be called, no matter how the with clause is executed. To make this work, I/O, locks, and database connectors had to be retrofitted with "__exit__" methods. This was done around Python 2.5-2.6; it took a while.
The result is that Python functions that open and close things tend to have this pattern:
def dosomething(args) :
try:
with *thingtoopen* as *handle1* :
with *otherthingtoopen* as *handle2* :
*do work on open things*
except EnvironmentError as message :
*report errors which occurred opening things, working on them, or closing them*
This is reasonably simple, and tends to result in programs that do something reasonable for error conditions. Importantly, even if an exception is raised, the close operations take place. Even the case of a close operation raising an exception is properly handled; the other close operations still take place, but they are aware there's an exception situation. A database connector would do a ROLLBACK instead of a COMMIT in such a situation.
This is probably the direction Rust should have taken. Error handling in Rust requires excessive boilerplate code every place an error can occur. The new Rust error hierarchy scheme [1] is especially verbose.
[1] http://lucumr.pocoo.org/2014/10/16/on-error-handling/