Our recent five-part tutorial on Google's Go language induced me to dip back into C-style programming. I was impressed with the improvements the Go team has made, particularly in the design of the return value mechanism. Unlike most languages, Go enables you to return multiple values from a function without creating some ad hoc data structure or object to do it. One of the standard return values is an error code, which is accessed conventionally upon the function's return through the
This solution solved a messy problem C's dual use of return values for data and error codes which was necessary due to C's lack of an exception mechanism. To be fair, this problem still exists in a different form in languages that have robust exceptions. For example, in Java, the conventional use of a null return both as an indicator of an error condition and as an actual data item laces codebases with endless tests for null. The problem is so ubiquitous in Java that many JVM scripting languages include shorthand to abbreviate the null checks.
- [ESG Report] Endpoint Security Must Include Rapid Query & Remediation Capabilities
- Want to Stay In Front of Breaches? Train Like The Marines.
- Tools & Techniques to Disrupt Lateral Movement
- Overcoming Cyberthreats to Critical Infrastructure with Integrated IT/OT Cybersecurity
In fact, Go has an exception mechanism as well, but its use is contrary to convention and convention is a central aspect of Go development. It's also not as elaborate as the exception mechanisms in C++ or Java. Its use is supposed to be for truly exceptional circumstances. I believe this is due to Google scale issues (recalling that Go was designed primarily to address Google development needs, rather than programming problems at large). Namely, that when you're running thousands of transactions on very large systems, exceptions become a very costly proposition. Not only are they slow, but they permanently fork the execution path. And on large, fast-moving systems, both effects tend to be highly undesirable. Moreover, exceptions have to be handled by future code that depends on current exception-oriented code. Because of these limitations, Google is fairly strict about limiting the use of exceptions in its C++ codebase:
"On their face, the benefits of using exceptions outweigh the costs, especially in new projects. However, for existing code, the introduction of exceptions has implications on all dependent code. If exceptions can be propagated beyond a new project, it also becomes problematic to integrate the new project into existing exception-free code. Because most existing C++ code at Google is not prepared to deal with exceptions, it is comparatively difficult to adopt new code that generates exceptions.
Given that Google's existing code is not exception-tolerant, the costs of using exceptions are somewhat greater than the costs in a new project. The conversion process would be slow and error-prone. We don't believe that the available alternatives to exceptions, such as error codes and assertions, introduce a significant burden.
Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we'd like to use our open-source projects at Google and it's difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well."
The position is unequivocal and the logic is hard to fault. But return values, even in the refined form found in Go, have a drawback that we've become so used to we tend to see past it: Code is cluttered with error-checking routines. Exceptions here provide greater readability: Within a single
try block, I can see the various steps clearly, and skip over the various exception remedies in the
catch statements. The error-handling clutter is in part moved to the end of the code thread.
But even in exception-based languages there is still a lot of code that tests returned values to determine whether to carry on or go down some error-handling path. In this regard, I have long felt that language designers have been remarkably unimaginative. How can it be that after 60+ years of language development, errors are handled by only two comparatively verbose and crude options, return values or exceptions? I've long felt we needed a third option.
One such option would put all the error handling code of a method in a separate section at the end, and the programmer would tie each error-handling snippet to the code via a single statement in the snippet. Then, for example, you could write
open( filename ) without any error-checking ceremony, knowing that any errors (be they manifest as exceptions or return codes) would be handled in the linked routine in the error-handling section. This might appear to be a super-local exception handler, but in fact, since it occurs at the language level, it could be implemented as something far simpler, such as code injection of the error-handling code back to where it is currently written longhand by developers. Language designers can implement such conveniences far more elegantly, I hope. The result would be that working code is much, much more readable. (If you have other solutions, please post them in the comments section.)
Go's innovation is indeed a step in the right direction. And other languages, such as Haskell with its
Maybe return value, have shown imagination in this area, but I think far more needs to be done to break out of the legacy mindset that error handling should be done as it always has been.