 Bonjour. Comment ça va? Ça va bien? So my name's Ben, and I'm here to talk about JavaScript errors. Really, this is kind of a deep dive into the error object and how stack traces are generated. And a whole bunch of that so we can come away, hopefully learning a little bit more about how errors work so we can use them more effectively in our own applications. I work for a company called Sentry. We're one of the sponsors here today. Sentry is a mostly open source platform, web service and client library collection for servicing errors from many different platforms, be they Node.js, or Ruby, or Unreal Engine, or iOS. Obviously, here to talk about JavaScript. If you are curious, it's very simple. You just sort of NPM install a library. Initialize that in your Express application or browser JavaScript application. And once you do that, once any kind of errors are thrown, you kind of get these really nifty dashboards that give you stack trace with surrounding source code, event volume, et cetera. It's on GitHub. You can run it yourself. I'll talk a little bit more about it at the end. Oh, there's some other screenshots. So if you've ever struggled with situations like the following, where maybe you're looking on your console or an error tool at Sentry and you see uncaught object object, what does that mean? Not very helpful. Or maybe you've got an error and it's just a message and there's no stack trace. Very frustrating. Don't understand how that happened. Or you've seen this. This is like a browser JavaScript error called script error, in which you know something's wrong, but that's pretty much as far as it goes. If you're suffering from some of these symptoms, I'm hopeful that we can learn some things to fix that. So in this talk, I'm going to talk about the error object itself, how you can extend that to create custom errors. The stack property and how stack traces are generated, what does it mean to throw other objects besides errors? And then a little bit about now that you know how the error object works and how you try and catch on all this jazz, how can you report on those errors to like a logging facility or some other tool? So if this doesn't interest you at all, you should make way right now. So the error object itself, where it all begins. So this is from MDN, the error constructor creates an error object. Instances of error objects are thrown when runtime errors occur. The error object can also be used as a base object for user-defined exceptions. Pretty concise, really want to focus on that. Instances of error objects are thrown when runtime errors occur. So what that means is that, let's say that you have code like the following. It's written sort of backwards, that's intentional, but just take a moment to read it. It's kind of like this cascading waterfall of function calls, where A calls B, B calls C, and then C calls this function D, which doesn't exist. And as we all know, that's probably not gonna work. Something's gonna happen. And it's gonna look like this. Uncover reference error, D is not defined. This is what you'd see, maybe your node application will just stop and throw this out, or you'll see this in the browser console. Thing is, is in the background, if you were to do this instead, you can actually throw your own reference error. There's nothing special about that. It's an error object that exists or is subclass from error, and you can throw it. And if you did this, you actually, it's the exact same stack trace. You wouldn't know the difference, okay? So all this is to say is that when you actually, throw a runtime error like this because your code doesn't execute, behind the scenes it's really just creating an error object and instantiating it, and then throwing it. So you don't have to, obviously, you don't have to throw reference errors. You can throw the base error object itself. And if you do that, you can see how the output is a little bit different. In this case, you see uncut error. This is super broken, but the same stack trace generates from that point. The error object has three major components. In my mind, it has the names. That's like, just as we saw reference error, that's the name of the error. That's actually a property on the error object that you can inspect. There's the message, and that's the first parameter that you pass to the error constructor. And then the stack is also a property. And these are actually values that you can inspect on the error object itself. So if I went back to that example, and if instead of throwing the error and sort of letting that bubble up to, letting the program crash, I instead try and catch it, I can inspect the values of that error object and output them to the console, and you can see that they look like this. So just that you know that these are things that you can navigate and inspect yourself. There's a bunch of standard errors that are kind of built into JavaScript. I don't know if all these are in Node. They certainly are in the browser, right? For example, URI error, that probably exists. Not sure, but these all derive from errors and they sort of are built into the runtime, some of which you may have not seen before. Internal error is when the actual underlying JavaScript engine has exploded in a way that it doesn't quite understand. So if you do see that, it means that V8 is not happy. Eval error happens if you try to run Eval in the global scope, things like this. You could use these yourself if you wanted to. Again, they're not protected in any way that you could throw them yourself. What is perhaps more practical though is to sort of extend the error class yourself to create your own custom error classes that might actually add more contextual information that you might wanna add. So in this very simple, super simple example, all I'm doing here is extending the error object. Just giving it a different name. You actually have to overwrite that name property. JavaScript doesn't magically know to look at that class variable and to sort of transport that to the name properties, so you have to declare it yourself. And now you can throw this error and it will operate the same way as every other error example we've seen, okay? This example right here doesn't really do much other than to create a new name of an error that could be useful. Maybe a more useful example is if you were building an API and you could create your own error class called the API error class and you can augment that at a status code. Maybe you could add some headers or some other values. And the reason you would do that is that maybe when you're inspecting these values later in your application, you're dumping them, you're console logging them, you can actually output that information which could tell you a little bit more than just sort of a stack trace. Below, this is sort of an example where you can try some code, you can catch it and you actually see if it's an instance of this API error in which case you can sort of like, you know that you can carefully extract that status property that wouldn't otherwise exist. Okay, so that's just the error object. It's not too, nothing too crazy. I think the most interesting part is the stack trace. It's the most useful thing. It shows us what were the frames that were executing before the thing broke. It's the thing that's gonna tell you where your application stopped working. So like I mentioned, it is like a property on the error object. It's actually like a prototype property. So how does a stack trace get generated? Some people, I don't know, if you're not aware, the stack trace is actually generated at the moment at which the error object is instantiated. Some people might believe that it's actually when you throw the exception as in the examples that we had. So if you have this example, it looks like earlier, the difference here is that I'm creating an error and then I'm returning it through the function waterfall all the way to the top and then throwing it there. The throw point doesn't matter because the error was created in that C function, the stack trace that you can see there in the bottom right still kind of cascades the same way. So the big takeaway is that it's on error creation is when the stack trace is generated. But that means that this is a pattern that sometimes we use in application development which is just sort of like catch a generic error and because we know more about it, we might re-throw one of our customers. So for example, what if I caught an error and I re-threw my API error and I want to add like a status code. If you do that, oops, sorry, getting out of myself, if you re-throw an existing error, you won't actually change the stack trace, right? So I can catch it, I can throw it again and there's no problem, it'll look exactly the same. So you can catch and throw and catch and throw, the stack trace will still originate. This is what it's getting into which is if you re-throw this as a new error object, right, like my custom error, it's getting out of myself, sorry. In this case, you actually generate a new stack trace and that's because you've created a new error object and instantiated that point and this can be problematic if you're doing this like not carefully, because you're actually eliminating all that original context that you were catching. So something to be aware of if you've ever dealt with a stack trace and you feel like it's missing frames, this is a possibility. What you can do instead is that if you are throwing your own custom errors, you can actually copy over the original error stack trace onto that new like custom error object that you've created in this case of calling it like original stack. And this way you kind of get both. You get the stack trace leading up to your custom error but you also get that original stack trace that bubbled up. You can also just completely overwrite it if you want, there's nothing stopping you from doing that. Everybody following so far? I'm going too fast, okay, a lot on it, good. Yeah. So everything that we've been talking about so far are these synchronous code examples but we all know the JavaScript has like a lot of asynchronous code and it's one of the things that makes it cool, challenging. One problem with that is that the stack property, it really only bubbles up to like the top most asynchronous frame. So let's say that we have the same example, A calls B, B calls C but then C kind of breaks out this set timeout function which queues up a call that gets executed asynchronously and then in that function that gets executed we now try to call the function D which doesn't exist. In this example, unfortunately, you just get this, okay? The fact that there was this sort of like cascade of function calls that queued all this code up, it's lost and that really stinks, very frustrating. This is something you should keep in mind. Now, you might be thinking, this is a node conference, I've heard a lot about node 12, it has asynchronous stack traces as a thing. Full disclosure here, I'm not like a full time node developer, it's more of a curiosity for me, I'm mostly like a browser JavaScript developer but I was playing around with this and this example, this example right here, if you try this in node 12, you'll actually get the same result. You've lost sort of like that, the outer context calling that set timeout but there are some patterns in that you can use in node 12 which actually give you like a full, like asynchronous stack trace, so if you're using promises, you're using async await and you have this example which is a little, it's actually very subtly different from the other one. Node 12 will sort of provide you this asynchronous call stack. It doesn't capture every sort of asynchronous call stack, there's just sort of like particular patterns that it's aware of in which it can generate this and it's usually using async and await. Chrome, however, if you have DevTools open and you use that earlier example that I had, this example doesn't try catch the run time error, it instead just lets it explode. If you run this in Chrome today in DevTools, you actually get this really nice output that is pretty handy, it has a different format but it shows you these sort of asynchronous calls and this is really great, this gives you kind of everything you need to figure out why the code was broken. A challenge here though is that this is actually just like the developer console, it isn't actually, even though Chrome and VA kind of has all the information it needs to sort of augment that error object with a stack property that would have this information, it doesn't. This is strictly just what you get in the console output and if you were sort of like logging this in the example that I had before, you'd lose these frames. This is all really silly, okay? So like different, obviously like different run times interpret error stack traces in different ways and that's because this property, this stack property that's so useful that we've depended it on, it's actually non-standard. The presence of it, the idea that it exists, that there is some property named stack, that is standardized but what is inside of it is sort of like a free for all, it's not defined. So perhaps in the future, all those asynchronous sort of like patterns will emerge or they'll get like extended onto the stack property for the error object and that would be fine because it's not standardized so there's nothing really saying that it can't. What that means today is that if you look at stack traces from different sort of like browsers and different JavaScript run times, they all kind of output these stack traces in different ways. So Chrome, V8 sort of output for example, they redundantly output the error name and the message on top of the stack. So this is me just outputting the stack property it includes that redundant information. Safari omits that information, has a different way of sort of like showing you frames. Firefox is another way as well. What this means is that if you're doing sort of like error reporting and you want to kind of like group different errors together, you have to do a whole bunch of like regex massaging and things if you were so inclined. So stack property, couple key takeaways, it's generated instantiation, rethrowing the new error, the same error will not generate a new stack trace but if you create a new error, obviously you will. The first frame is the top most asynchronous execution except for in some cases, it's just a string, right? So everything I've shown you, this is not some clever way or like array that you can manipulate, it's just all string data as non-standard. So so far I've been showing these examples where we throw errors, which is like the really good way of throwing things if you want to stop execution. In other languages, if you try to throw or raise an exception using a non-error object, that act in of itself will create an exception. So in this example with Python, if I try to raise a string, it basically is like, whoa, whoa, whoa, you can't do that. That's not how this language works. JavaScript, however, is perfectly quite content to let you throw anything. So in this example, similar as before, functions A, B, and C and then I throw this string called oops at the very top. If I do this and I try to catch it and I try to log it, you just get this. You just get the string oops. There's no stack trace, there's no message, there's no nothing. And so if you've ever been logging errors and you see just random strings or things that you don't understand, oftentimes this is the case. It doesn't have to be a string, it could be an object. It's like any non-error object or nothing but the stack property, you'll get something like this, object to object. Now it's not very common for people to say, throw a string. What I think is more common that I've seen is that when people are doing promise rejections, they might say, hey, I'm gonna reject this promise with a null value or a negative one or something. And if you do that, it's the same story. You're kind of deriving yourself, or not deriving, you're preventing yourself from doing like post hoc debugging of that data because there's no stack trace. So if you are working with promises and you have an opportunity, you should create error objects as opposed to just resolving things with like primitive values or you know, you don't know. There are global like promise handlers that can help you find things that don't resolve. So that's only gonna be a useful utility for you if you actually have a stack trace to help you understand where those rejections occurred. If you know that this is happening in your code, you'd like to stop it. There are actually some like ESLint rules that you can add to your software, code repository right now that'll look for these patterns and encourage you to resolve promises with error objects. And if you're interested with that, there's no throw literal that's preventing you from like throwing strings and objects and then prefer promise reject errors is another rule you can use to stop that. So earlier we learned about how you know, you can use this fact that when you create an error object or you instantiate it, you actually get a stack property. You can use that in useful ways. So in this example, let's say that you know that you have a string that's thrown somewhere and this is really challenging because you don't know where it is, right? This is a very contrived example where I'm throwing a literal value, but what if you're throwing some variable that's being passed around, right? Very hard to track down. A technique that you can use is that you can try and catch some problematic code. And if you know that this error is not an instance of an error, you can actually throw a new error at that point in time, in which case to kind of like generate what I call like a synthetic trace where you create a stack trace up to the point where you caught it. And that's not obviously like, it's not gonna get you all the way to the root of your problem, but it at least gives you kind of like a starting place. And from there, you can kind of like add more try-catch statements with these synthetic traces to kind of get you closer to the problem. There's other useful ways of using this like stack property. Like if you just want to, you don't have to throw an error, right? You can just create an error object, console log the stack or put the stack properties, preserve it in a way that is useful for you and toss the error away. That's perfectly valid as well. So non-error exceptions, they're causing you to throw any non-error. They make determining root cause difficult because you got no stack trace, promises, common source of them, and you can use this error object to help you out. So up until this point of kind of talking about the error object, ways to use it a little bit more effectively or at least understand how it's operating so you know what you're looking at. And I talk really briefly and just like, what if you want to report on those errors? Here's a really contrived example, like if I try some code, problematic code, it blows up, I can catch it and then I can use fetch or node fetch to send some data to an error reporting endpoint. This could be a logging endpoint or something you've stood up. I'm not gonna go into the details of that. Of course, you don't really want to pepper your code with try catch statements, especially because we try catch only works on asynchronous boundaries, you'd have to do it all over the place. So most JavaScript runtimes provide you like global error facilities, error collecting facilities that you can use to collect them. And node, you can use process.on uncaught exception and you can use that, catch an error, try to make a HTTP request somewhere. But if you do this, you should be really painfully aware that this actually subverts your program halting. So you have to do it yourself and the docs will tell you that you should really only run synchronous code in this error handler because if it doesn't exit, there's a chance that your application might be still running in some sort of like undefined state while you're trying to do cleanup. So something to keep in mind, like I would argue even this really contrived example I wrote is not very safe because it's basically saying, okay, the program halted, I'm gonna just get to send this error report somewhere. And while I'm waiting for that to return, my application is gonna run in undefined state and then I'm gonna process exit. But I just wrote this as just like a really contrived example because dealing with this in a safe way is too complicated for one slide. Similarly, there is, you can use that same sort of event handler for unhandled rejection. The callback for this is it takes a reason, which is why was this actually, why was this promise rejected, which is typically an error. And if it is an error, you can of course extract it and send it just like in the example before. In this case, this doesn't halt operation of your program so you can kind of do this much more safely. In browser world, it's a little trickier. Browser world has this window.onair global handler that is really like a relic that dates back all the way back to the 90s. And so it's sort of just kind of been, we've just been tacking on parameters until it's been useful. So in order to preserve backwards compatibility, so that's why it has all these arguments. So if you're curious, the very first iteration just took like a message and then it took a URL, then it took a line. Then we're like, yeah, column would be useful too. Let's throw that on there. And then finally, we just sort of added this like final fifth error object that actually contains everything. So just like before, you can on window.onair, you can kind of send this data somewhere. The browser also has an unhandled rejection, global error or global rejection handler. It has a little bit different signature than what we saw earlier. It takes like a promise event and on that event, there is a reason value that hopefully is an error object and you can report on it that way. And also what's really cool about this is Firefox in the last couple of months just added support for this. So it's actually a relatively newer thing, but now every browser supports all of these global error tools, which is pretty cool. One quick example, I do want to bring up that in node world listen, you run your whole environment. It's very easy. You just collect errors, throw them somewhere. Browser world is a little tougher. Here's an example of like actually using this on air handler where I'm loading a script with my example code. And very importantly, this is coming from a CDN. So I'm pointing, it's very common for us to publish static code onto CDNs these days whether that's Netlify or something else. And if you do this this way, you might come into this problem where if I throw some code, instead of getting the stack trace on this cool like error object that we were talking about, you don't get anything, you just get script error, okay? This is a problem that a lot of people run into when they're configuring this. So I think it's always like worth taking a minute to talk about. It's very challenging because it is basically omitting everything. So that null object that I've kind of highlighted in the output, that's the error object. So the browser has actually said like, you can't see this. I'm not even gonna show you what the error is in this situation. And the reason it's doing that is because of the same origin policy. So same origin policy is this sort of policy that makes it so windows and tabs can't communicate between domains. You don't wanna like leak information from your bank website to some other website. And this actually extends all the way to global errors. And this kind of makes sense if you think about it because what their fear is is that if you can load a script from another website and you're actually authenticated on that website then it's possible that an error might be thrown from that website that contains private information about your account, right? Like, you know, uncaught error, user Ben Vinegar whose account details are one, two, three, four, five, six, seven, eight, nine. You know, this is their password. It's obviously like super contrived. I don't imagine that would happen but it is a possibility. And so for that reason, if there's an error that would come from a script that's hosted on another domain it actually gets sort of like basically blanked out with the script error. The way to get around it is you use cores if you're familiar with it, cross origin, resource sharing. You have to do it in two ways. If you're requesting this script from a CDN you have to set this attribute called cross origin equals anonymous. That's actually a signal to the browser to say this is like, the expectation is that this is sort of like a cookie list domain. You can just request this. There's no session information. And I believe the browser literally doesn't send cookies if you sort of like request a resource this way if it's set up this way. And then on the other side from your CDN or whatever's, you know, web server that's returning this script you have to actually declare that any origin can request this, right? So you say access control our origin asterisk or you can lock it down to a set of domains that are trusted. And this is sort of like, you know, it's almost like a handshake, right? The browser is saying, I'm not gonna request anything with session information. And the server is also saying I have no expectation that there will be any session information here as well. And once both of those things have to happen then this example that I showed you earlier it'll work. You get to actually see errors that come from other domains. So just something to keep an eye out for. So it's kind of like the end. What did we learn? That's a good question. I don't know. You can use custom errors to add meaning to failures if you're not already. You should keep in mind how stack traces are generated because if you're logging them you might have a better idea now of kind of like why does it have this shape and where did the error like occur from? Don't throw strings, objects are the non errors because you'll find yourself in this very problematic situation where you don't know what's going on. And you can use global exception handlers to report errors if you want to. This is of course the part where I suggest that you shouldn't do that at all. You should just use like a tool that does it for you because it's really complicated. And I don't have enough time to go over the ways in which it is complicated. So Sentry is, this is kind of what we do and it's a source available software. So that's basically, it's like fully permissible for you to run, modify yourself, you can run on your own servers. It is on GitHub on github.com slash get sentry slash sentry. Gives you cool error reports like this. I usually don't mind pitching this because like the whole idea here is that you can run it yourself. And here's a kind of an example of that it exists. We have a booth here and if you're curious to see what it looks like I'd love to talk about it. If you thought any of this was interesting you wanna learn just go a little bit further into how does our Sentry JavaScript library work? You can go ahead and take a look at the source code over here. If you are curious more about the script error thing or you wanna do your own error collection over in some blog posts that you can check out. So that's it for me. I hope this was okay. Thanks so much for being here. Merci beaucoup. Thank you.