This guest post comes from Brett van Zuiden, CEO and Founder of Filepicker.io, a product for developers that helps their web and mobile applications better work with user content, providing an SDK that performs accelerated uploading, image conversion, and integration with cloud sources like Facebook Photos, Dropbox, and Google Drive.
Philosophy of communicating errors - “Guard Rails” or “Read the Manual”?
Of course, adding code to handle errors in a graceful way comes at a cost - the extra code must be maintained by the distributor of the library, and the resultant library will take a some amount of performance hit due to the extra logic and an increase in size. An alternative is to set the expectation that should errors occur, the developer should look back to the documentation to ensure the inputs are correct, etc. Obviously this depends on having very good documentation. It also depends on having a developer base that is comfortable navigating the documentation, won’t be discouraged by exceptions that occur as they’re starting out with the library, and a willingness to use debuggers to navigate inside your libraries code to figure out what’s really going on. In our case at Filepicker.io, we made a conscious choice to do substantial validation and fail gracefully whenever possible to encourage ease of adoption and reduce support overhead, while still breaking out the majority of the error messages into a separate debug module to reduce code size. There may be cases where you choose to forgo this type of graceful failure for raw performance, but it may cause substantial headache down the line as people unfamiliar with the code start working with it.
Exceptions, Special Returns, or Error callbacks - how to communicate what went wrong
In general there are three types of errors: programming errors, user errors, and system errors. Take for example the following code for asking for someone’s age:
Programming errors are when the writer of the code made a mistake, such as forgetting to pass in a prompt to askForNumber, therefore making the question variable undefined. A user error is when an action taken by the user isn’t within the bounds of what was anticipated, such as writing “thirty” in response when a plain number was expected. A system error is when some element of the infrastructure is not behaving as it should - this is often a concern when making AJAX calls, but can sometimes come into play with DOM manipulation as well. For instance, if the browser was unable to render a prompt box.
These three types of errors arise in very different situations and should be handled in different ways, so the way your API communicates errors should reflect that. Programming errors should be made immediately apparent and offer help as to where the error occurred and how to fix it. User errors should also offer guidance as to what went wrong, potentially in user-friendly text, but more importantly should be easy for the calling code to handle gracefully. System errors should include information as to whether the issue is intermittent and the request should be tried again, or whether there was a permanent failure of a component. At Filepicker.io, we settled on the following convention:
- All programming errors (missing or invalid inputs, bad state) should throw errors, so that a stack trace is included by default and debuggers can be utilized effectively. We chose to create our own exception type rather than use a string or the built in Error type so we could allow for easier type checking and add additional capabilities.
- All user and system errors return specific error objects via callback functions. In cases where the function returns synchronously rather than asynchronously, special return values can be used instead of error callbacks. The error objects were all of a specific type (FPError) to allow for easy type-checking, and all included an error code which could be matched against in an if-chain or switch statement. In addition, the FPError object had custom toString() methods that would pull in messages from a debug library if available - see below for more details.
Because we limited exceptions to exclusively programming errors and passed back user and system errors (“handleable” errors) via an error callback, try-catch blocks are unnecessary, and in fact discouraged as they could easily mask bugs.
Debug Libraries - best of both worlds?
One way to keep the core library lightweight is to have a “production” version that doesn’t have any of the standard debugging messages or input validation, and a “debug” version that provides verbose output, checks that all inputs are appropriate, etc. This can be via a pre-compilation step that removes any debugging helpers before creating the production library, allowing you to create mylibrary.js and mylibrary.debug.js files, similar to how most open-source libraries offer minified and unminified versions, or as a standalone utility that hooks onto the main library and adds debugging help. We chose the latter, so that when they are still in development, the developer can include the filepicker_debug.js file which adds a number of debugging flags, including importing error message strings for the FPError.toString(). This way the bulky error message strings can be kept out of the production library, but still available at a moments notice if needed.