This is a quick rundown on key JavaScript Error
and Exception handling pitfalls.
Includes snippets which apply across the JS ecosystem, including Client-side, Back-end and everything in-between!
Everyone from rookies to Node Team members forget this shit all the time. Myself included. This is my attempt to collect my assorted debugging tips, reminders, tricks and magic.
- 👌 Shit
- Error Pitfall #1
Error.callStackLimit = 50
- FIX:
Error
to JSON is empty object
- Express
- Required: Request Logging (
morgan
, etc) - Required: 404 Not Found Handler
- Required: 500 Exception Handler
- Required: Request Logging (
- Promises (when you use
knex
,axios
,fetch
, etc it's Promises)Promise.reject()
- Catching Errors
- Never use
.then
's 2nd parameter
- React & React Native
- IDE / Environment
- Auto
eslint
- Auto Complete Tools
- Prettier: Auto Formatting
- Auto
- Code Samples
- Custom Error Types/Classes
- Util Methods
- toPlainObject - For getting all possible properties from an object.
- getTaxonomy - Print Class Inheritance Hierarchy
HOWTO: Correctly
throw
/new
Error
's
Similar to the topic in the Promises section on Promise.reject
Bad & Correct Examples:
// INCORRECT: DON'T THROW STRINGS!!!
throw 'Invalid Usage, May Fail Silently'
// CORRECT: Use `new Error()`
throw new Error('Will have call stack & other exception details provided in the ctx of the callsite')
Using correct method ensures you get best possible stack traces in any runtime/engine.
Error.callStackLimit
is a little-known gem of a feature.
Ever struggle to find your code in a stack trace?
Endless functions in node_modules
, yet none of your files?
Well, this fixes it 50-75% of the time: add the next line to the top of your entrypoint file (likely index.js
or app.js
).
// Opt #1: Add to the top of your first file - set before any Error might be created
Error.callStackLimit = 50 // IMPORTANT: Will kill performance - disable in production
// Opt #2: In Node Apps use something like this:
if (process.env.NODE_ENV !== 'production') { Error.stackTraceLimit = 50; }
Error.callStackLimit
works on both NodeJS & Browser apps!
Bonus: Here's another way to log a stack trace: console.trace('Print Trace of Function Sequence that got us here')
JSON.stringify
ing an Error
instance fails to include expected keys.
I've seen this cause problems in Chrome console logging, analytics/tracking libraries, and Express apps' res.send(json)
or res.json(json)
, etc.
Interview Question: What do you think this code returns?
JSON.stringify(new Error('Oh noes'))
Hint:
assert(err.message === 'Oh noes')
[.... think about it ....]
🕕
🕖
🕗
🕘
🕙
🕚
🕛
Answer: An empty object
{}
Do you know about
Object.keys()
's often forgotten cousinObject.getOwnPropertyNames()
?
Let's look at some example code.
Examine the 3 different log outputs:
const err = new AppError({httpStatus: 420, httpMessage: 'Traffic too hi', message: 'Sad Panda', stack: null})
console.log('.getOwnPropertyNames: ', Object.getOwnPropertyNames(err).sort().join(','))
// -> .getOwnPropertyNames: httpMessage,httpStatus,message,stack
console.log('.keys: ', Object.keys(err).sort().join(','))
// -> .keys: httpMessage,httpStatus
console.log('err: ', JSON.stringify(err, null, 1))
// JSON.stringify ->
// err: {
// "httpStatus": 420,
// "httpMessage": "Traffic too hi"
// }
Consider how
JSON.stringify
is likely implemented?
Get the important keys out of the Object/Error manually:
- Check out my simple fix function:
toPlainObject(obj)
- Some more express-specific solution(s) might look like either:
- Required:
const errorInfo = err => {error: 'Server Error', message: err.message, stack: err.stack}
- Pick one:
if (err) return next(errorInfo(err))
- Pick one:
if (err) return res.status(500).json(errorInfo(err))
- Pick one:
promise.then(fn).catch(err => next(errorInfo(err)))
- Plenty alternatives exist
- Required:
Install the morgan
logging lib.
It will log (to terminal) any requests made to your node HTTP server.
For all those times you wondered, "am I even requesting file X correctly?"
Rough usage example:
// Add to your Express App.js entry file
const devMode = process.env.NODE_ENV !== 'production'
const morgan = require('morgan')
// then add the following after your body-parsers
app.use(morgan(devMode ? 'dev' : 'combined'))
The last app.use(req,res[,next])
will become your defacto 'File Not Found' handler.
Makes sense, considering all middleware and routes are applied in the sequence they were setup.
// Add right before the final error handler middleware (see next tip)
app.use(function _notFound(req, res, next) {
console.error('File not found:', req.originalUrl);
res.status(404).send({error: 'Url not found.', url: req.originalUrl});
});
One method is add call a final app.use()
with a 4-argument function, like so: app.use((err, req, res, next) => {})
.
This will become your auto-magic error handler.
Makes little sense, but it works.
An approach I've found to be problematic uses something roughly like server.listen(3000).on('error', err => console.error('OnError:', err))
.
Note: While the server.on('error')
style may seem to work, I've had trouble losing stack traces and getting erroneous messages with certain tool combos (notable, yet junk libs like Q/$q
& async
are partly to blame).
// Express 4-arg Error Handler:
app.use(function _errorHandler(err, req, res, next) {
console.error('ERROR', err);
res.status(500).send({error: err.message, stack: err.stack, url: req.originalUrl});
});
Make sure you use correct error invocations:
// INCORRECT: DON'T THROW STRINGS!!!
return Promise.reject('Invalid Usage, May Fail Silently')
// CORRECT: Use `new Error()`
return Promise.reject(new Error('Will have call stack & other exception details provided in the ctx of the callsite'))
https://developer.android.com/studio/run/emulator-acceleration.html
Linting enforces per-project code styling rules. This should minimally be used in build steps or deploy/distribution tasks.
eslint
will catch bugs in your code and enforce consistency based off .eslintrc
config.
standard js
is another popular option - quite easy to setup - but it's more geared towards auto-fixable issues.
If you find a high-level debate about the virtue of linting. Ignore it.
It's the same as saying: I'm a great driver, so, not only don't I need headlights, I can skip those silly seatbelts too! Seems like a choking hazard anyway!
Driving habits aside, clearly we should at least avoid coding carelessly.
We might as well catch errors early, upon saving.
Atom Note: If Atom auto-save feature is slowing things down, add a 1-3 sec delay in the settings somewhere.
- ESLint The defacto standard js linter in 2016-2017+
# Install pkgs locally
npm install eslint@3.x babel-eslint@7 --save-dev
Example .eslintrc.js
:
module.exports = {
"extends": ["eslint:recommended", "plugin:react/recommended"],
"plugins": [
"prettier"
],
"rules": {
"no-var": 0,
"max-len": 0,
"prettier/prettier": ["error", { "trailingComma": "es5" }]
},
"env": {
"node": true,
"mocha": true
},
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"allowImportExportEverywhere": false,
"codeFrame": false,
"ecmaFeatures": {
"ecmaVersion": 6,
"jsx": true,
"experimentalObjectRestSpread": true,
},
}
}
- Standard JS Rapidly rising popularity.
-
VS Code has included great auto-complete out-the-box for a while!
-
Atom has many options... That said, I'd avoid all except
atom-ternjs
IMHO, this is one of few valid use-cases for the ES6
class
syntax. Inheritance withclass/extends
is just harder to mess up compared to older methods:Object.create
,Error.prototype
,util.inherit
, etc.
Lets outline & checkout example code:
- Error (built-in, extends `Function` built-in)
- HttpError (extends `Error`)
- AppError (extends `HttpError`)
- ServiceError (extends `HttpError`)
class HttpError extends Error {
constructor(details) {
super(typeof details === 'string' ? details : 'ERROR: message wasn\'t specified')
if (typeof details === 'object') Object.assign(this, details)
}
}
// Create 2 classes based off of HttpError
class AppError extends HttpError { }
class ServiceError extends HttpError { }
Goal: Merge all instance properties into a fresh POJO - this is inclusive of inherited members/keys. This is a
JSON.stringify
andres.json
friendly method.
TODO toPlainObject()
- Circular variable reference detection.
- Callback overrides
- Key exclusions
Example usage:
toPlainObject(new AppError({aborted: false, external: true, message: 'Test Error'})
// => {aborted: false, external: true, message: 'Test Error', stack: [...]}
// More advanced sample!
const e = new AppError({message: 'Test Error', stack: null})
console.log('message:', e.message)
console.log(' json:', JSON.stringify(e))
console.log(' keys:', Object.keys(e))
console.log(' gopn:', Object.getOwnPropertyNames(e))
console.log('Wrapped with toPlainObject:\n')
console.log(' json:', JSON.stringify(toPlainObject(e)))
console.log(' keys:', Object.keys(toPlainObject(e)))
/*
@summary
toPlainObject:
*/
function toPlainObject(obj) {
if (obj === null || obj === undefined || typeof obj !== 'object') return obj
return Object.getOwnPropertyNames(obj)
.reduce((output, key) => {
output[key] = _getVal(obj[key])
return output
}, {})
}
function _getVal(v) {
if (v === null || v === undefined) return v
if (v instanceof Date) return v.toISOString()
if (/Number|Boolean|String/.test(v.constructor && v.constructor.name || '')) return v
if (Array.isArray(v)) return v.map(toPlainObject)
if (typeof v === 'object') return toPlainObject(v)
return v
}
Goal: Merge all instance properties into a fresh POJO - this is inclusive of inherited members/keys. This is a
JSON.stringify
andres.json
friendly method.
Example usage:
getTaxonomy(new AppError('error').join(' -> '))
// => Error -> HttpError -> AppError
getTaxonomy(new Error('error')) // -> [ 'Error' ]
getTaxonomy(new HttpError('error')) // -> [ 'Error', 'HttpError' ]
getTaxonomy(new AppError('error')) // -> [ 'Error', 'HttpError', 'AppError' ]
getTaxonomy(new AppError('error').join(' -> ')) // => "Error -> HttpError -> AppError"
getTaxonomy(AppError) // -> [ 'Function' ]
getTaxonomy(parseInt) // -> [ 'Function' ]
// [ 'Function' ] Indicates obj NOT an instance
function getTaxonomy(o, taxonomy = []) {
o = taxonomy.length <= 0 ? o.__proto__.constructor : o && o.__proto__
return !o || !o.name || taxonomy.indexOf('Function') > -1 ? taxonomy : getTaxonomy(o, [o.name].concat(taxonomy))
}