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
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:
- Use
?.
optional chaingingvar age = req.body?.human?.age;
- Use
??
null coalescing (fornot defined
,undefined
, andnull
)age = age ?? -1;
- 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
- (never handle errors in the "happy path" function, especially not in multiple locations)
- 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);
- (a thunk is a function with that receives a callback with the error first, and then a single value:
- 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();
- With
async
andPromise
s, errors are just values - just like Goasync 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;
- 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
{
"human": {
"age": 12
}
}
app.use('/', function (req, res) {
var p = req.body;
var age = p.human.age;
age += 10;
res.json({ older: age });
});
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 });
});
value, err := ParseString(str)
if nil != err {
return nil, err
}
return value, nil
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);
}
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);
}
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 } });
}
});
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);
});
See https://github.com/therootcompany/async-router
app.use('/', async function (req, res, next) {
let result = await doStuffAsync(req.body);
res.json(result);
});