The sorry state of Javascript error handling – Throw all the things! – Logging from the browser



The sorry state of Javascript error handling – Throw all the things! – Logging from the browser

0 0


vilniusjs-20150618-errors

The sorry state of Javascript error handling

On Github dominykas / vilniusjs-20150618-errors

The sorry state of Javascript error handling

By Dominykas Blyžė (@dymonaz), 2015-10-28

inwh.se/win

var Table = require("throatable");
throw Table;

Throw all the things!

But then what?

survey: who is logging to server? who is building for 3rd party sites? who has 3rd party snippets?

Javascript is 20. Ready for production.

Seriously, browse the web with the developer tools console open logging errors and be truly amazed that the internet ever worked.

@shanselman

Handle all the things?

92% of catastrophic failures are the result of incorrect error handling

Simple Testing Can Prevent Most Critical Failures by Ding Yuan et al, dominykas.net/24

Pro-Tip

Make sure stuff works without JS:

  • Use real <a href> with real URLs
  • Use real forms with real buttons
  • preventDefault() as a last thing

Error handling in the normal world

Stay aware of errors Provide detailed error information Add exception handling code If all else fails - crash with a dump

In the browser world...

  • Old browsers
  • 3rd party scripts
  • Browser extensions
  • Cross-domain restrictions

In the browser world...

  • throw new Error vs throw old_Table
  • Errors in async functions
  • Errors in callbacks
  • Zalgo

In the browser world...

  • Network errors
  • Aggressive caching
  • Random obscure 💩

Random obscure 💩

  • XHR randomly status 0 (but error logged CORS XHR)
  • <iframe id="localStorage" />
  • localStorage max size is 0 in private mode on iOS
  • getComputedStyle stops being a function...
  • Random message events from plugins

Randomer obscurer 💩

document.createElement is not a function

Randomest obscurest 💩

Function.prototype.apply was called on function (){ /* snip */ }, which is a function and not a function

Logging from the browser

Do this tomorrow

If you're not doing it yet

window.onerror = function (err, url, line) {
	ga('send', 'exception', {
		'exDescription': line + " " + err
	});
};
						

Do this next week

  • Set up a proper logging endpoint
  • Version your releases and pass on the version
  • Make sure versions match across different bundles
  • Don't include "data" inside the "message", i.e. log(messageString, objData);

Pay attention

Anything can be thrown - must be able to serialize correctly.

var logMessage = err && err.message ? err.message : "" + err;
> null.toString()
TypeError: Cannot read property 'toString' of null
> new Error("Damn it").toString()
'Error: Damn it'
> new Error("Damn it").message
'Damn it'
						

Try

At the very least they have useful blogs.

  • Rollbar
  • Errorception
  • TrackJS

Dream

Content-Security-Policy: style-src cdn.example.com; report-uri /_/csp-reports

Catching errors

try/catch

  • Ugly
  • Sync
  • Deoptimizes *

* no big deal most of the time, but still

guard()

function guard (fn) {
	return function () {
		try {
			return fn.apply(this, arguments);
		} catch (err) {
			logError(err);
			throw err;
		}
	};
}
						

Using this for auto-wrapping is probably a bad idea.

Use promises

  • Wrap promise construction
  • Reject with new Error()
  • Don't forget to .catch(logAndRethrow)
  • Stack traces an issue (unless native)
  • New: unhandledRejection
  • Problem: UI can't have a break in event loop

window.onerror

  • msg is different on every browser
  • Yay! Uncaught Error: undefined is not an object
  • url has cross-domain restrictions
  • lineno is 1, because you minified
  • colno is fairly recent
  • errorObj is very recent

Meanwhile at in unnamed browser vendor HQ...

People are adding too much shit to their HTML, so let's, like, invent something other, something Accelerated, like for Mobile and Pages, like, which is like HTML, but really not HTML, and they won't add shit there and then it will be, like, very accelerated and mobile and pages stuff.

window.onerror

  • Not the same as addEventListener('error')
  • TraceKit for stacks, if you can afford it
  • Run your code in an iframe, if on a third party site
  • return true if handled

window.onerror - CORS

Access-Control-Allow-Origin: * <script crossorigin="anonymous">

window.onerror - CORS

  • The script can inject itself with the attribute set
  • setAttribute("crossorigin"), not script.crossorigin
if (!document.currentScript.getAttribute("crossorigin")) {
	var script = document.createElement("script");
	script.src = document.currentScript.src;
	script.setAttribute("crossorigin", "anonymous");
	document.head.appendChild(script);
} else {

	/* proceed */

}
						

Source maps

browserify \
	-e bundle/entry.js \
	-t uglifyify \
	-d | exorcist bundle.js.map > bundle.js
						

Source maps

var SourceMapConsumer = require("source-map").SourceMapConsumer;
var map = JSON.parse(fs.readFileSync("bundle.js.map"));
var smc = new SourceMapConsumer(map);
console.log(smc.originalPositionFor({ line: 5, column: 33 }));
						

Output

{
source: '~/bundle/stuff.js',
line: 2,
column: 7,
name: null
}
						

Embulact

  • $exceptionHandler
  • onerror
  • Replay events

Throwing errors

throw errors

If you don't throw, it won't have a stack in IE

throw errors

  • If it's not an error, it won't have a stack
  • This applies to nodeback(err, data)
  • Object.create(Error) is not an error

Use error codes

var error = new Error("snap...");
error.code = "E_SNAP";
throw error;
						

throw custom errors

I don't want to talk about it...

throw custom errors

MyCustomError should:

  • Keep message as a first param
  • Have a message property
  • Have a name="MyCustomError" property
  • Extend Error (i.e. support instanceof)
  • Work with and without new

Add useful info

Good custom errors also should:

  • Have a reasonable toString
  • Be able to wrap around existing errors
  • Have a stack trace

Poor man's stacktrace-js

function CustomError(message) {
	var temp = Error.call(this, message);
	temp.name = this.name = 'CustomError';
	try {
		throw temp;
	} catch (afterThrow) {
		this.stack = afterThrow.stack;
	}
	this.message = temp.message;
}
CustomError.prototype = Object.create(Error.prototype);
						

Number of boring stack lines

  • In Blink: name/message + 2
  • In Webkit: 1
  • In Firefox: 1
  • In IE: name/message + 1

</ノಠ益ಠノ彡┻━┻>

Slides: dominykas.net/25

We're hiring: inwh.se/win

@dymonaz

;