Error handling and control flow (in JavaScript)
Nicolae Vartolomei · 2016/01
These are the slides I have presented at a JSMD meetup. This happened shortly after joining Retently, where my focus was building a robust product foundation on Node.js/JavaScript platform.
The goal was to present a high level overview of error handling and control flow, how these relate to one another in an asynchronous language like JS, and where the language design is heading.
Sentences starting with ^ are presenter notes.
Exception handling
Process of responding to the occurrence, during computation, of exceptions – anomalous or exceptional conditions requiring special processing.1
Control flow
The order in which individual statements, instructions or function calls of a program are executed or evaluated.2
try {
runAsync(function (err, res) {
if (err) throw err // throw exception (1)
console.log(res)
})
} catch (ex) {
console.error(ex) // exception isn't caught (2)
}
^ JavaScript’s asynchronous nature.
Normal Developers
JavaScript Developers
JavaScript has the fastest growing ecosystem
Error handling is one of the most important—and overlooked—topics for programmers.
^ Especially it is overlooked in JS ecosystem. Projects built using JS are pretty flat. Most of the time it is ok to fail completely.
^ We decided to build some complex software, using multi-layered architecture, and other enterprise design patterns which lead to a good amount of layering (composition).
^ We rely on a high amount of software other people write.
^ We want a robust ecosystem.
^ It’s hard to reason about production level code, if it’s full of workarounds and verbose error handling code.
Best practices
Error first callback (implicit)
^ Why JS error handling patterns fail.
Error first callback
aka “bad news first”
Error first callback
aka “bad news first”
function fn( err, [data, .. ] ) { .. }
// Function that takes 2 arguments
// - First argument is an error
// - Second argument is the result
// Never pass both
// Error should be instanceof Error
// Return value is ignored
// Must never be called more than once
^ We want predictable code paths.
// everything in my program before now
someAsyncThing(function () {
// everything in my program for later
})
^ Trust lost
- Don’t call my callback too early
- Don’t call my callback too late
- Don’t call my callback too few times
- Don’t call my callback too many times
- Make sure to provide my callback with any necessary state/parameters
- Make sure to notify me if my callback fails in some way
In Java everything is an object. In Clojure, everything is a list. In JavaScript, everything is a terrible mistake.
The First Important Discovery of the 21st Century
JavaScript has good parts.
Promises (A+)
Huh, we’ve got formalization.
^ Explicit contract. We have spec!
The thing is, promises are not about callback aggregation. That’s a simple utility.
^ Many see Promises as panacea to callback hell.
^ Comparing apples to oranges.
The point of promises is to give us back functional composition and error propagation in the async world.
^ More importantly, if at any point that process fails, one function in the composition chain can throw an exception, which then bypasses all further compositional layers until it comes into the hands of someone who can handle it with a catch.
^ We can short circuit and throw an exception.
^ Inversion of Control.
doWork()
.then(doWork)
.then(doError)
.then(doWork)
.then(doWork)
.catch(errorHandler) // catching here
.then(verify)
^ We have finally for cleaning up.
If a particular library does not return promises, it’s trivial to convert using a helper function like Bluebird.promisifyAll
.
Drawbacks
Everything is wrapped in an implicit try...catch
block.
Promise.prototype.catch
will catch all the errors.
^ Things that you shouldn’t handle: stack overflow, API interface changes. Things that you can’t solve at runtime.
No .finally
in the official spec.
^ Backwards compatibility.
Bluebird
ES2015
generators *
Single-threaded, synchronous-looking code style
^ Allows to hide the asynchronicity away as an implementation detail. ^ This lets us express in a very natural way what the flow of our program’s steps/statements is without simultaneously having to navigate asynchronous syntax and gotchas.
function* foo() {
try {
const x = yield request("https://google.com/")
console.log("x: " + x) // may never get here!
} catch (err) {
console.log("Error: " + err)
}
}
var cache = {}
function request(url) {
if (cache[url]) {
// "defer" cached response long enough for current
// execution thread to complete
setTimeout(function () {
it.next(cache[url])
}, 0)
} else {
makeAjaxCall(url, function (resp) {
cache[url] = resp
it.next(resp)
})
}
}
^ But it will quickly become limiting, so we’ll need a more powerful async mechanism to pair with our generators, that is capable of handling a lot more of the heavy lifting. What mechanism? Promises.
tj/co ||
mozilla/task.js
^ TJ Holowaychuk ^ spawn
co(function* () {
try {
yield Promise.reject(new Error("boom"))
throw new Error("boom") // definitely boom it
} catch (err) {
console.error(err.message) // "boom"
}
})
One of the most powerful parts of the ES6 generators design is that the semantics of the code inside a generator are synchronous, even if the external iteration control proceeds asynchronously.
ES2016
async/await
^ ES2016 Stage 3. There are 4 stages in total, which means it is almost ready.
async function main() {
try {
const quote = await getQuote()
console.log(quote)
} catch (error) {
console.error(error)
}
}
^ Weimer and Necula “exceptions create hidden control-flow paths that are difficult for programmers to reason about”
So, use try/catch everywhere, right?
^ The reason JavaScript errors get thrown in our faces so often is that code is often not written in a robust manner.
^ Nothing could be less graceful than a try/catch around every line of code, or worse yet, just one big try/catch wrapped around everything. You should design your code to be as robust as possible, and use try/catch clauses in specific areas where it will be helpful and more graceful than just dying in the code execution.
Error handling tips
Operational errors vs. programmer errors
^ Operational errors represent run-time problems experienced by correctly-written programs. These are not bugs in the program. In fact, these are usually problems with something else: the system itself (e.g., out of memory or too many open files), the system’s configuration (e.g., no route to a remote host), the network (e.g., socket hang-up), or a remote service (e.g., a 500 error, failure to connect, or the like).
^ Programmer errors are bugs in the program. These are things that can always be avoided by changing the code. They can never be handled properly (since by definition the code in question is broken).
This distinction is very important: operational errors are part of the normal operation of a program. Programmer errors are bugs.
Specific recommendations for writing new functions
Be clear about what your function does
^ what arguments it expects ^ the types of each of those arguments ^ any additional constraints on those arguments (e.g., must be a valid IP address)
^ If any of these are wrong or missing, that’s a programmer error, and you should throw immediately.
Use Error objects (or subclasses) for all errors, and implement the Error contract
^ Do not callback with strings, do not reject with strings, do not throw strings
^ All of your errors should either use the Error class or a subclass of it. You should provide name and message properties, and stack should work too (and be accurate).
throw {
name: "SyntaxError",
message: m,
at: at,
text: text,
}
function CustomError(message, extra) {
Error.captureStackTrace(this, this.constructor)
this.name = this.constructor.name
this.message = message
this.extra = extra
}
require("util").inherits(module.exports, Error)
Use the Error’s name property to distinguish errors programmatically
^ When you need to figure out what kind of error this is, use the name property. Built-in JavaScript names you may want to reuse include “RangeError” (an argument is outside of its valid range) and “TypeError” (an argument has the wrong type). For HTTP errors, it’s common to use the RFC-given status text to name the error, like “BadRequestError” or “ServiceUnavailableError”.
^ Don’t feel the need to create new names for everything. You don’t need distinct InvalidHostnameError, InvalidIpAddressError, InvalidDnsServerError, and so on, when you could just have a single InvalidArgumentError and augment it with properties that say what’s wrong (see below).
^ Do not use http error codes.
If you pass a lower-level error to your caller, consider wrapping it instead
const VError = require("verror")
const err1 = new Error("No such file or directory")
const err2 = new VError(err1, 'failed to stat "%s"', "/junk")
const err3 = new VError(err2, "request failed")
console.error(err3.message)
References
- https://en.wikipedia.org/wiki/Semipredicate_problem
- https://www.youtube.com/watch?v=UsBng6yfiEQ
- MountainWest JavaScript 2014 - Error Handling in Node.js by Jamund Ferguson on YouTube
- https://strongloop.com/strongblog/robust-node-applications-error-handling/
- https://davidwalsh.name/async-generators
- https://davidwalsh.name/concurrent-generators
- http://blog.getify.com/promises-part-2/
- http://blog.getify.com/concurrently-javascript-1/
- http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html
- https://nulogy.com/who-we-are/company-blog/articles/from-promises-api-to-async-await/
- http://www.2ality.com/2015/03/no-promises.html
- https://www.joyent.com/node-js/production/design/errors