Elapsed

00m

Node.js error handling

@Raynos

We have all seen code like this


function myApp(data, cb) {
    doSomethingAsync(data, function (err, result) {
        computeResult(result, function (err, result) {
            cb(null, result)
        })
    })
}
      

Can you spot the mistake?

Adding error handling is easy


function myApp(data, cb) {
    doSomethingAsync(data, function (err, result) {
        if (err) return cb(err)

        computeResult(result, function (err, result) {
            if (err) return cb(err)

            cb(null, result)
        })
    })
}       
      

Something a little more complex


function createIfNotExist(file, content, cb) {
    fs.stat(file, function (err, stat) {
        // if err is a no such file then create it
        if (err.code === "ENOENT") {
            fs.writeFile(file, content, cb)
        } else if (err) {
            cb(err)
        } else {
            cb(null, null)
        }
    })
}
      

Error handling in web apps


function handleRequest(req, res) {
    parseJSONBody(req, res, function (err, body) {
        if (err) return DO_SOMETHING

        model.fetchStuff(body.id, function (err, record) {
            if (err) return DO_SOMETHING

            res.end(JSON.stringify(record))
        })
    })
}
        

Error handling in web apps


function handleRequest(req, res) {
    parseJSONBody(req, res, function (err, body) {
        if (err) return res.end(err.message)

        model.fetchStuff(body.id, function (err, record) {
            if (err) return res.end(err.message)

            res.end(JSON.stringify(record))
        })
    })
}
        

Can you spot the problem?

The solution?


function handleRequest(req, res, opts, cb) {
    parseJSONBody(req, res, function (err, body) {
        if (err) return cb(err)

        model.fetchStuff(body.id, function (err, record) {
            if (err) return cb(err)

            res.end(JSON.stringify(record))
        })
    })
}
        

Don't shoot errors into space

  • res.end(err) is not the same as cb(err)
  • Your process should be handling errors
  • It's even worse if you don't handle errors in the browser

Logging your errors

  • Store the req url + headers
  • Store the session context
  • Store a unique error id
  • Store the name and args of the method

A logError function


var logger = bunyan.createLogger({ ... })

// in router.js
function logError(req, res, err) {
    logger.error({
        methodName: value.name,
        error: extend(value.err, {
            stack: value.err.stack,
            name: value.err.name
        }),
        id: err.id,
        args: value.args,
        req: req,
        res: res,
        user: req.user
    })
}
      

JSON errors


// server.js
function handleRequest(req, res) {
    model.fetch(function (err, data) {
        if (err) return sendJson(req, res, {
          message: err.message
        })

        sendJson(req, res, { data: data })
    })
}
      

// browser.js
request({ ... }, function (err, res, body) {
    if (body.message) {
        if (/Invalid email/.test(body.message)) {
            validationElem.textContent = "Invalid email"
        }
    }
})
      

Errors are not strings


// server.js
function handleRequest(req, res) {
    model.fetch(function (err, data) {
        if (err) return sendJson(req, res, {
          message: err.message,
          type: "invalid.email",
          attribute: "email"
        })
    })
}
      

// browser.js
request({ ... }, function (err, res, body) {
    if (body.type && body.type.substr(0, 7) === "invalid") {
        validationElems[body.attribute].textContent =
            "Invalid " + body.attribute
    }
})
      

Transparent logging


module.exports = function fetch(name, cb) {
    db.cache.get(name, function (err, value) {
        if (err) {
            logger({ name: "cache.get", args: [name], err: err })
            return cb(err)
        }

        cb(null, value)
    })
}
      

Transparent logging


function logged(obj, logger) {
    Object.keys(obj).forEach(function (key) { var fn = obj[key];
        obj[key] = function () {
            var args = slice.call(arguments), cb = args.pop()

            return fn.apply(this, args.concat([function (err) {
                if (err) logger({
                    name: key, args: args, err: err
                })

                cb.apply(this, arguments)
            }]))
        }
    })
}
      

The subtle errors


function handleRequest(req, res, opts, cb) {
    getSession(req, res, function (err, user) {
        if (err) return cb(err)

        fetchRecords(user.email, function (err, records) {
            if (err) return cb(err)

            sendJson(req, res, records)
        })
    })
}
      

Can you spot the mistake?

The subtle errors


function handleRequest(req, res, opts, cb) {
    getSession(req, res, function (err, user) {
        if (err) return cb(err)

        // TypeError: Cannot read property 'email' of null
        fetchRecords(user.email, function (err, records) {
            if (err) return cb(err)

            sendJson(req, res, records)
        })
    })
}
      

Classic null property access errors

Domains to the rescue

domains allow you to handle thrown exceptions.

Think of them as uncaughtException but per req/res

Domains to the rescue


var domain = require("domain")

function handleRequest(req, res) {
    var fn = matchRoute(req, res, router)
    var d = domain.create()
    d.add(req)
    d.add(res)
    d.on("error", handleError)

    d.run(function () {
        fn(req, res, {}, handleError)
    })

    function handleError() { ... }
}
      

Crashes without any errors

Sometimes your process goes down and there are no errors

Thankfully you will have a core dump

If you are on joyent just open mdb on the core dump and have a look

Questions?

@Raynos