Skip to content

Instantly share code, notes, and snippets.

@coolaj86
Last active January 2, 2022 03:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save coolaj86/d494512d71bbbef845ffc52f432e1848 to your computer and use it in GitHub Desktop.
Save coolaj86/d494512d71bbbef845ffc52f432e1848 to your computer and use it in GitHub Desktop.

Re: NodeJS vs Go error handling

This is in response to Kai Hendry's NodeJS vs Go error handling

  • 0:00 Storytime: TJ leaves node because it sucks
  • 0:25 Example: A simple service to increment a number
  • 0:40 - 0:51 Node.js: A Naive Route Implementation
  • 1:07 - 1:24 Node.js: Things go to hell when the structure changes
  • 1:24 - 1:53 Go: Fantastic (that's true)
  • 1:53 Node.js: try/catch hell (and you can't tell)
  • 2:44 CTA: If you know better, tell me

TL;DR Tips

JavaScript is more complex than Go. It's much harder to handle errors well and to ensure that errors are handled. Here are some tips for JavaScript in 2021 that will work in Node LTS and (old) Evergreen Browsers, without transpiling:

  1. Use ?. optional chainging
    var age = req.body?.human?.age;
  2. Use ?? null coalescing (for not defined, undefined, and null)
    age = age ?? -1;
  3. Use next(err) in express with the built-in error handling
    • (never handle errors in the "happy path" function, especially not in multiple locations)
      // use async / await so that "errors are values"
      app.use('/', function (req, res, next) {
        Promise.resolve().then(async function () {
          let result = await doStuffAsync(req.body);
          res.json(result);
        }).catch(next);
      });
    • (the error handler is function (err, req, res, next) {})
      // utilize express' error handling
      app.use('/', function (err, req, res, next) {
        console.error(req.method, req.url, err);
      
        res.statusCode = 500;
        res.json({ error: { message: "something bad happened, but it's our fault" } });
      });
    • For even cleaner routes, use the lightweight async-router wrapper
  4. Use require('util').promisify to upgrade any callback "thunks" to promises
    • (a thunk is a function with that receives a callback with the error first, and then a single value: function (err, val) {})
      function doStuff(options, cb) {
        // ...
        if (err) {
          cb(err);
          return;
        }
        cb(null, value);
      }
      
      var doStuffAsync = require('util').promisify(doStuff);
  5. JavaScript supports multiple return values, similar to Go
    async function getFruit() {
      if (false) {
        return [ "", -1, new Error("no fruit for you!") ];
      }
      return [ "apple", 1, null ];
    }
    
    let [ fruit, quantity, err ] = await getFruit();
  6. With async and Promises, errors are just values - just like Go
    async function parseStuff(str) {
      // ...
      if (false) {
        throw new Error("parse error ...");
      }
      return result;
    }
    
    let result = parseStuff(str).catch(Object);
    if (result instanceof Error) {
      throw result;
    }
    return result;
  7. Use uncaught error handlers
    window.addEventListener('error', function (err) {
      // ...
    });
    process.on("uncaughtException", function (err) {
      // ...
    });
    process.on("unhandledRejection", function (err, promise) {
      // ...
    });

Most Go libraries look and feel similar, most of the third-party code I’ve worked with so far is high quality, which is sometimes difficult to find with Node since JavaScript attracts a wider range of skill levels. - https://medium.com/@tjholowaychuk/farewell-node-js-4ba9e7f3e52b

That's a euphemism for:

Go attracts seasoned Software Engineers whereas most JavaScript developers are either new to software development and have no idea what they're doing, or they're from a different language and refuse to adopt the JavaScript paradigms, no matter the cost

It's difficult to learn JavaScript well because there simply aren't as many great resources. But we do have Crockford on JavaScript, The Complete 9-Video Series

How to check JSON

Scenario: Known-Good Input ❌

{
  "human": {
    "age": 12
  }
}
app.use('/', function (req, res) {
  var p = req.body;
  var age = p.human.age;
  age += 10;
  res.json({ older: age });
});

Scenario: Unknown Input ✅

It doesn't have to "go to hell" just because the structure changes.

{
  "age": 12
}
app.use('/', function (req, res) {
  var p = req.body;
  // Optional Chaining: `?.`
  // null Coalescing: `??`
  var age = p?.human?.age ?? -1; // 🚀
  res.json({ older: age + 10 });
});

How to use Go-style Error handling

Actual Go Style

value, err := ParseString(str)
if nil != err {
  return nil, err
}
return value, nil

Multiple Returns in JavaScript (Overly Go-ish)

It's not popular, but it works

function parse(json) {
  try {
    return [ JSON.parse(json), null ];
  } catch(e) {
    return [ null, e ];
  }
}

With an error:

let [ result, err ] = parse('{ "some" "error" }');
console.info(result);
if (err) {
  console.error("had a problem:", err);
}

Without any errors:

let [ result, err ] = parse('{ "foo": "bar" }');
console.info(result);
if (err) {
  console.warn("Had a problem:", err);
}

The happy medium

async function parse(json) {
  return JSON.parse(json);
}
async function main() {
  let result = await parse('{ "some" "error" }').catch(Object);
  if (result instanceof Error) {
    console.warn("Had an problem:", result);
    return;
  }
  console.info(result);
}

Typical n00b Express.js Route

app.use('/', function (req, res) {
  try {
    doStuff(req.body, function (err, result) {
      if (err) {
        res.json({ error: { message: err.message } });
        return;
      }
      res.json(result);
    })
  } catch(e) {
    res.json({ error: { message: err.message } });
  }
});

Typical Expert Express.js Route:

doStuffAsync = require('util').promisify(doStuff);

// use async / await so that "errors are values"
app.use('/', function (req, res, next) {
  Promise.resolve().then(async function () {
    let result = await doStuffAsync(req.body);
    res.json(result);
  }).catch(next);
});
// utilize express' error handling
app.use('/', function (err, req, res, next) {
  console.error(req.method, req.url, err);
  
  var e = Object.assign({}, err);
  e.message = err.message;
  e.stack = err.stack;
  
  res.statusCode = 500;
  res.json(e);
});

Squeaky Clean Express.js

See https://github.com/therootcompany/async-router

app.use('/', async function (req, res, next) {
  let result = await doStuffAsync(req.body);
  res.json(result);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment