 Hello, and welcome to my talk about waiting, and things not working. So before we begin, this is me. When I'm not working, I often fly these weird little planes that don't have engines. And it turns out, if you don't have an engine, there is no engine that can fail. When I'm not flying, I do a lot of open source development, usually in the AMBA ecosystem. If you want to know more about me, you can find me on GitHub and Twitter. Sometimes I even get paid to work on AMBA projects. I've been working for SimpleApps for about four years now. And you may know SimpleApps from popular AMBA add-ons such as AMBA SimpleAuth, AMBA TestSelectors, and Q&A DOM. We're a small consultancy agency in Munich, Germany. But we're working with clients all over Europe and sometimes also the US. But let's get back to the talk. This talk could have also been called Loading States and Error Handling. But that doesn't sound quite as catchy. The third topic we will talk about is testing. Because we need to make sure that our Loading States and Error Handling code doesn't break six weeks from now. Let's start by looking at a simple example application. This is a button. If we click it, a network request will be triggered. That sounds easy enough, right? This is the template of our button component. There are no surprises here. We have a plain button element, and that has an on-click modifier. Clicking the button will call the like action on the component class. This is the component class, and it's not too complicated either. We have a like action, and that triggers the network request. And that's basically it. But as you can see, there is no Loading State, and there is no Error Handling like it. So let's change that. In this first chapter of the talk, we will look at implementing Loading States. And more importantly, I will also show you how to write tests for them. So back to our example button. What we want to implement is a Loading State. I can look roughly like this. The button should be disabled, and the text of the button should change to please wait. So how can we do this? We can implement this in our component template by introducing an in-progress property. We directly assign it to the disabled attribute of the button, and we will use a condition in the button to change the text if the property is true. On the JavaScript side, we will implement that as a track property, which we set to false by default. We will set it to true before starting the request. After the response comes back, we will finally reset it to false. But what happens when fetch fails? In that case, the like action will stop running, and our property will never be reset. That is not what we want. We want to always reset it. We can fix this by using the try finally pattern. We will put the fetch call in the try block and reset our property in the finally block, which always runs, whether fetch fails or succeeds. Have you ever seen an assertion message that reads calling set on a destroyed object? This is related to another problem that we have in our code here. If the component is already destroyed when the fetch response comes back, we will set the track property on the destroyed component, because we can't cancel the async function. We can work around that assertion by checking the isDestroying and isDestroy properties before running any more code. But we will have to do that after every single async step. And this is easy to forget and not really ergonomic, right? So what we can do instead is we can use the Ember concurrency add-on. It is a good alternative that fixes this automatically. We can replace the action decorator and async function with the task decorator and generator function, as you can see on the slide here. We have changed the like action to like task. The advantages that generators can be canceled and tasks will automatically be canceled when the component is destroyed. We can also use the derived state that each task provides so that we don't have to manually keep track of our in progress property. This change simplifies our code quite a bit. I would strongly recommend to give this add-on a try if you've never used it before. If you want more information, the add-on has excellent interactive documentation page on emberconcurrency.com. Before we get to the arrow handling part of this talk, let's talk a bit about writing tests. So to get started, we will write a simple happy path test for our component. First we call the setup rendering test helper function. We create a new test. We render our component. We click it and then finally we assert that the counter shows the expected number of likes. Can you see the problem in this code? The problem is here. When we click the button, it will cause a real API request. And unless we are explicitly building end-to-end tests that involve a real backend, we should mock our API requests in some way. Mocking the request helps us build reliable tests that can be repeated many times without having to worry about the state of the backend database. Luckily there are plenty of solutions for mocking API requests. The three most popular ones in the ember ecosystem are Pretender, Mirage, which is built on top of Pretender, and PolyJS from Netflix. If you want to see other options, you can take a look at the ember observer website. For this talk, I've chosen Mirage, since that is what I'm most experienced with. To use it, we first import the setup Mirage test helper. We call it right after calling the setup rendering test helper. That allows us to use this dot server in the test code to set up road handlers, for example. In this snippet here, that means that each post request to slash like will return a JSON object with five likes in it. And then finally, as last step, we adjust our assertion to match our mocked API response. Now let's look at the loading state tests. We can pass a timing option to Mirage, which will tell it to delay the response for a number of microseconds. In this example here, we've chosen 500 milliseconds, sorry. We've also changed our assertion to check that the button is disabled and that the label has changed. The problem is this doesn't work. The assertion fails. But why? The reason is that click and other test helpers implicitly wait for network requests to finish before continuing with the test code. That means our assertion runs after the loading state is already gone again. We can solve this by removing the await from our click call and don't wait for it to finish. Instead we use the wait for helper and wait for the button to be disabled. At this point, we can run all of our loading state assertions. Finally, we wait for the app to settle, which means network requests and rendering are finished. And then we can check that the loading state is gone again. But now we have a slow test. And this doesn't really scale if you do it for every loading state in your app. If we decrease the delay, we might introduce a race condition that happens if the rendering of the loading state takes too long. We want to tell Mirage to continue immediately after the loading state assertions have run. A nice solution for this problem is using the defer function from the RSVP library that Ember apps shipped by default. We first call defer to create a deferred object. We then pass the deferred promise to our rod handler. And after the loading state checks have run, we call defer.resolve to tell Mirage to continue. The result is a much faster test without any race conditions. Let's summarize this first part of the talk. First make sure to use library or an add-on to mock your API calls. When testing loading states, make use of the wait for test helper and remove the await in front of the regular test helper. Use await settle to also check that loading states are reset correctly. And finally, use the defer function to avoid race conditions and speed up your tests. Now we know how loading states work and how they can be tested. It's time to talk about error handling. And let's start with writing tests. When we pass a number as a third argument to Mirage, it specifies the HTTP status code that will be returned. We can set it to 500 here to return an internal server error, for example. At the end of the test, we can then assert that an error message popped up. And the error message, we obviously haven't implemented yet. And how do we implement it? Should we add a place for error messages to every template in our app? That seems like a lot of work and that can make the app look inconsistent to the users. A good solution for this is using a global notification system. This means having a service that displays notifications at a single location on top of the app. As you can imagine, this is much less work than having a dedicated error element in each of your templates. Here's a small list of add-ons that provide such a system. mbcliflash, mbclinotifications and mbonodify. For this talk, I've chosen to use mbclinotifications. But the others all work roughly the same way. And it looks like this. First, we inject the notifications service. Then if the request fails, we call the error method on the service to show an error notification. By default, these notifications stay on the screen until the user explicitly closes them. But for most cases, we want to hide them after a few seconds. And we can use the autoclear option of mbclinotifications for that. But if you use that, make sure to disable this behavior for testing. If you don't, the tests will wait until the notifications are hidden, which leads to very slow tests again. Let's go back to our error handling code. There's another problem here. Fetch can actually fail in two ways. The case that we have covered here already is an HTTP response with a non-200 HTTP status. For example, an internal server error. The other case is an actual network error. For example, when going into the subway. In this case, fetch throws a type error, and we haven't handled that one yet. The solution is to make non-200 status responses throw errors too. We can then wrap everything in a tri-catch block, and we can move the notification into the catch block. Now we have a single place that handles both kinds of errors. To test this, unfortunately, we can't use Mirage, because it does not appear to have the functionality built in to test network errors. But we can override and mock the fetch function manually instead. Here, we override it with an async function that throws a type error, just like the real browser would. But we need to make sure that we reset the smoke after the tests. Otherwise, all following fetch requests in other tests might also fail, and that's not what we want. We can solve this by implementing a small test helper. Let's call it setupFetchRestore. It saves the regular fetch in a variable before the test, and after the test it resets it back. In our test module, we can use it similar to how we call the setupMirage function. This should make sure to always reset the fetch function, even if a test happens to fail. Let's move to a slightly different topic, error reporting. And by that, I mean systems that let you know when your app fails in production. And it can look like this, for example. This is how an error report looks like on SentryIO, which is one of the popular error reporting services. You can see that it displays a lot of different data about the error. The most important ones are the error message and the stack trace at the bottom. But it also shows us the browser and operating system of the user, and if you set it up correctly, it even shows you the Git commit revision that your app was built from. As I said, Sentry is just one popular example of such a service. Other services like this are Bugsneck, TrackJS, Rollbar, and there are plenty more available. I have the most experience with Sentry, so that is what we will be using for the following examples. We don't have too much time here, so I will not get into the details on how to set up Sentry. We will only focus on how to use it. If you want to know how to set it up, I will refer you to a blog post on the SimpleApps blog that explains it in detail. I've put the URL in the slides here. The following examples will assume that we've already set it up correctly. Let's look at how to use it. By default, all uncaught errors and promise rejections will automatically be sent to Sentry, but we can explicitly send errors to Sentry too. We first import the addSentry slash browser package, and then we call the CaptureException function and pass in an error object. But do we really want to send all of our errors to Sentry? This can create a lot of noise. Let's go back to our subway problem. When network errors happen, there is not much we can do about it on the app developer side, aside from showing an error notification, obviously. So we should probably not send these issues to Sentry because we can't do anything to fix these. Let's look at the different types of errors that can happen. As we discussed, network errors can basically always happen. Server errors are similar and also something we can expect to happen from time to time. For client errors, it depends. Sometimes a 404 error is legitimate. Sometimes it tells us we're doing something wrong on the client side. We need to figure out what errors are actionable from the developer side and only send those to Sentry. We want to make sure to display error notifications to the user for all types of errors. But to reduce the noise on the error reporting system, we should only send unexpected or actionable errors to a system like Sentry. We can do this by checking the error type before sending it to Sentry. And as we discussed, we don't want to send network errors or server errors because those are not really actionable for us. But these kinds of functions don't exist yet. So how can we implement them? As mentioned before, fetch throws a type error for network problems. And we can use an instance of check to figure out if the error is a type error. But unfortunately, we have no way of telling if it is an actual network error or if it is a different type error. We could check the error message string. But obviously, all the browsers use different strings for that. We could do nothing, or we could wrap the fetch in a function and throw a custom error class instead. In the end, it is a complexity trade-off, and I will let you decide how strict you want to be with that. For the server side error case, we will make a small adjustment to our response.ok condition code. Instead of throwing in regular error, we will throw an HTTP error instead. And you may wonder, what is an HTTP error? HTTP error is a custom error class that we can implement ourselves. In this case, the constructor here takes a response object, and it will automatically generate a appropriate error message for that. It also saves the response status as a property on the error object itself. This allows us to implement an isServerError function that checks if the error is an HTTP error and that the status is in the 500 range. To summarize, I recommend to use a notification system to make displaying errors easier for yourself. You should also use some kind of error reporting service to not run blind in production. When dealing with fetch, it can be useful to throw custom error classes like our HTTP error here. And finally, you should only send action of the errors to your error reporting service. And that's it. Thanks for listening, and I hope it was useful to you.