Check out a free preview of the full HTMX & Go course
The "Error Handling" Lesson is part of the full, HTMX & Go course featured in this preview video. Here's what you'd learn in this lesson:
ThePrimeagen discusses the problem of displaying form errors and handling form submissions in an HTMX application. They demonstrate how to handle form errors by returning a 422 status code from the server and updating the form with error messages. They also show how to use the `hx:BeforeSwap` event to customize the behavior of HTMX when handling 422 responses.
Transcript from the "Error Handling" Lesson
[00:00:00]
>> So there we go. We have this nice little post. When we make a post, this is what happens to our contacts page. This kinda sucks. Can we all agree that this sucks? This is not what I want to see at all, right? This is not it. This is not any of it, in fact.
[00:00:13]
This is horrible. And if I keep on adding, it'll just keep on getting worse and worse and worse and worse. And look at that thing go, it's just awful, right? So this isn't beautiful. I bet you, though, that a lot of people can now guess what happened. What happened?
[00:00:31]
>> I have no idea.
>> You have no idea, but we have already solved this problem.
>> We're not targeting the div.
>> Say it out loud, come on.
>> We're not targeting the div.
>> Yeah, we're not targeting the right content, I love it. I know, right, John?
[00:00:44]
I knew you were gonna get it correct. I could feel it, you were ready, you were just there. All right, so yeah, that's exactly it. Look at our response, we're responding with the whole darn thing. And what is it doing? Who made the request in this situation? The form made the request.
[00:01:00]
So therefore, the form gets replaced. Even if we go to the HTML, we might see that our form has a form, that has a form, that has a form, that has a form. Our forms had babies. Look at that. Look at how many it had. So we did something wrong here, right?
[00:01:13]
We need to start targeting and thinking about, how are we gonna update this page? And this is actually quite an interesting problem, we're gonna get to the problem of form and display all in one page. Let's see, I don't know what it says. Yeah, so let's fix this problem.
[00:01:27]
So this is the same problem as before, we're returning the whole thing. So first and obvious and easiest thing we can do, if you're lazy and you just wanna get a product out there, just hit it with the old hx-target="body". My goodness, fantastic, I'm refreshing the page. Did you see how fast that compiles?
[00:01:44]
Look, my goodness, it's the easiest thing ever. Look at that, we're just adding stuff, it's updating. Not a big deal, super, super simple. By the way, I just love how fast, the compilation is just so good. All right, is this good? What are some problems with this approach?
[00:02:02]
Can someone give me a problem with what we're doing right now? John.
>> Are we sending the full HTML?
>> We're sending all the contacts and everything. What's the problem?
>> I'm Chad, John says losing focus if you are targeting the body.
>> You can lose focus, yes.
[00:02:22]
So there's an entire concern on focus and all that kinda stuff. You can totally fix all those things, we're not gonna be really be talking about that. There's obviously autofocus. HTMX, if a child component has focus, will ensure that focus is retransferred to the same item. There's a whole bunch of stuff around focus.
[00:02:39]
There's also the whole accessibility concern. We're not gonna be really focused on that, no pun intended. Instead, we're gonna be focusing more on just the HTMX side of things. Cuz that really is a JavaScript slash frontend problem, it's not really an HTMX problem. Again, it's the same reason why we're not thinking about styling.
[00:02:58]
It's just not a problem of HTMX.
>> The payload get bigger the more trips that happen?
>> Yes, so if you couldn't hear it, the payload keeps getting ever bigger. Meaning if you had 1,000 contacts, we have to send down 1,000 contacts plus a form every single time you add one.
[00:03:13]
That's a horrible problem to be in, we don't wanna be there. So we're gonna explore the space just a little bit, okay? We're just gonna get in there. We're gonna see what happens because this seems kind of exciting. Yes, I know some people are suggesting the fix being Rust, 1,000 contacts, not a big deal with Rust, right?
[00:03:29]
Still a problem. Trust me, this is a problem, let's not do that. So let's first start by only responding with contacts. That should be a pretty easy reduction of stuff. So let's jump back in here, and we need to change two things. We need to change who are we targeting and then the code that's being generated.
[00:03:46]
So I'm gonna go like this, hx-target, and let's go down here and call this thing id="contacts", right? And then we're gonna jump back here and we're gonna go #contacts. All right, awesome. So now that we have that, that should do all the redirecting. So now we're gonna go over to the server and we need to say, hey, don't render the index, render the contacts, right?
[00:04:11]
Or wait, I call it display. Did I call it display? Yeah, I called it display. Terrible name, but you get the idea. Okay, awesome, we're gonna do that. So I'm going to refresh this, and let's add a contact. Okay, I mean, that's better, right? We're getting better, I'm just creating so many sweet contacts.
[00:04:31]
Is this better? In some sense, it's better. But let's take a little gander at what's happening. Look at what happened here, we're having multiple levels of contacts. Why is that? Can anyone tell me why we're having multiple levels of contacts?
>> Doing inner HTML.
>> Inner HTML, boom.
[00:04:52]
Did you know, HTMX, you can say how you want to replace it. So let's just jump up here and let's do a little hx-swap="outerHTML". So instead of replacing the inner contents, let's replace the entire element here for a second. So let's at least fix that problem. So I'm gonna do that, do that.
[00:05:13]
And boom, as we added a bunch, notice that we no longer have that problem. So we can kinda specify how we wish to swap out the elements, which is a pretty cool little item. So for those that didn't see it, on the form, I did hx-swap. And so this is just how you dynamically say, this is how we want to change things out.
[00:05:32]
Pretty cool, pretty great, very happy about that. I'm happy, you're happy, hopefully. Let's keep being happy together. All right, so is that good? What's wrong with this approach again? There's two big things. There's two very glaring problems here. We already kinda discussed the first one. What's the first one?
[00:05:50]
>> First one is returning multiple users.
>> Yeah, we're returning way too many users, it could get really bad. What's the second obvious problem with this approach?
>> It doesn't clear out.
>> I mean, I just added pretty much the same contact over and over again. There is absolutely no error handling at all.
[00:06:08]
What happened if what we put up there and you can always bypass the client, right? So you cannot even think that client validation is a real topic to be trusted. You have to do all your validation on the server. So the two big problems is, a, we're sending all the contacts, and b, we have no way to specify how errors are delivered back to the client.
[00:06:28]
Maybe this contact already exists, maybe you didn't enter in a valid email. We got some problems here. So I already did this. Well, one option is we could use something called response headers. If you don't know what a response headers are, in HTMX, you can actually do some server-side overriding of how things should happen.
[00:06:45]
So I can say, hey, we're gonna retarget our response to the form and deliver down some errors. We could re-swap. We don't wanna outer HTML swap, we want to inner HTML swap. Or however we wish to do it, we could do any sort of reswapping, retargeting, re-all the things.
[00:07:03]
I don't wanna do that, but that is an option if you wish to do that. I find that feels a little bit more confusing for me, feels a little bit harder to kinda grasp what's happening, feels like it could be a little goofy. Another option would be out of band updates.
[00:07:16]
So this is pretty cool idea where HTMX, you can actually say, hey, go update this other place as well. So okay, so let's make the response be an error on duplicate email and displays errors in the form, 400 status code for bad request, right? Let's use proper status codes.
[00:07:36]
Let's have this really proper experience. I just wanna make sure. Okay, awesome, let's do that. So what we're gonna do is we're gonna go over here. And luckily, we were very, very smart earlier, we did a little type alias. So I'm gonna do a little func, a little c *Contacts, and then we're gonna go hasEmail, why not?
[00:07:58]
This is probably not the way you'd wanna do it, but this works out pretty much, all right? Oopses, we don't need to do a little star right there. You don't wanna have a pointer of a pointer, it just seems really bad. Invalid receiver, can we grab one of those on here?
[00:08:13]
Can we not do that? I thought we could do that on this one. Am I crazy you can't do that? I swear you could. This is just such disappoint, such disappoint. Okay, we can't do that. I might be rusting inside my head. All right, fine, we'll do it on the data, whatever, [SOUND] whatever.
[00:08:33]
Sometimes I put in too many things in my head. All right, there we go. So we went over, we're gonna do hasEmail on the data. It's gonna go over all of its contacts, see if it can find a contact that's emails true, and there we go. Now, awesome.
[00:08:49]
Let's go. So we're happy about that. So we're gonna go down here and I'm going like this. If data.hasEmail, I want to render something back. I want to render some sort of error, I wanna say, hey, you're a 400, hey, things have gone wrong. So I'm gonna replace this with the 400.
[00:09:04]
We're gonna render a 400. And now, we need to display some sort of error here. So we have that thing called form. You may remember our well-named form right here, but we're gonna need to have some more information stored here. I want to be able to return down a form that has its previous values, and I also wanna be able to return down a form that has some errors within it.
[00:09:28]
And so let's jump back here and let's do that. So I'm gonna create something called type FormData, which is a struct, which has a values, which is gonna be a map[string]string. And we'll have Errors map[string]string. Just keep it simple for now, pretty generic. Maybe not the way you want to do that, that's fine.
[00:09:46]
And then I'm gonna have a newFormData. Hey, thank you very much. Which is just simply gonna say, hey, we're gonna make those things. Cuz if you don't make a map, you may not realize this. It's one of Go langs many downfalls, is that if you don't make a map, it's a nil pointer, and things go kaboom.
[00:10:00]
It's just so annoying. Just give me a zero value map, that's all I want in my life. I know gophers are gonna come out and be like, well, actually, did you know the reason why? I don't wanna hear your reason why, it makes me angry, okay? All right, now that I got done with that, fairly useless- [INAUDIBLE]
[00:10:20]
>> Okay-
>> Shake my finger.
>> Yeah, I shake my finger at the silly things, right? All right, so now that we got that, well, actually. All right, so now that we did that, let's think about how we want to do this form data. Well, we don't care if this looks pretty, so I'm just simply gonna go like this.
[00:10:36]
if .Values.name, yeah, we'll keep it lowercase, why? It is what it is. value="{{ .Values.name }}", easy, awesome. We'll do the exact same thing right here, and we'll replace name with email, all right? So if we have an email, put the email in the value, all right? Pretty easy, I know a lot of people are like, well, what about placeholder?
[00:11:00]
Again, we're not trying to focus on the beauty aspect here, we're just trying to focus on the practical use of HTMX. So now we need to do something really important, which is if .Errors.email, well, then we need to display this wonderful error message. So yeah, look at that, I'm gonna erase this.
[00:11:19]
We even got ourselves a red error. So we're gonna have an email error that says, hey, there was an error here. We're gonna display this as the form, it's now part of our form. Forms, by the way, are awful. They always turn into this. They're always just a huge nightmare of just previous values and filling things in and doing all that.
[00:11:35]
So fun times, I've never met a form I liked. So now that we have form looking like this, we're probably gonna need to make several changes here. All right, so be prepared. So let's jump back to our server. Let's first deal with this part right here. Word like this, formData := newFormData().
[00:11:55]
Beautiful, I'm gonna go formData, Values is name. I'm gonna do formData, value is email. And then I need to do formData.Email. Look at that. Copilot, look at that, it's like you knew what I was doing. We're gonna add in the error, emails already exist. Easy peasy, pumpkin seedsy, pass in the formData.
[00:12:13]
But now that we've done that, we've also screwed up our index page. You can no longer refresh cuz everything is gonna have all these errors. These properties don't exist. So let's create a quick little index page right here, type, I'll just call it Page, why not, struct. And it's gonna have Data, which is the Data, and we're gonna have Form, which is the formData.
[00:12:32]
Easy peasy, look at how beautiful this programming is. It's really not beautiful, no one should be impressed by this. By the way, everyone should be fairly upset about this. Instead, so now we're just gonna do a nice little newPage, just so I don't have to do any of that.
[00:12:45]
Can I just do this? Yeah, newData, there we go. And then just hit him with one of those. Look at this, just the world's greatest programming going on here. So instead of using a data, let's use a page, and go newPage. There we go. We have our empty page, it's absolutely fantastic.
[00:13:01]
We're gonna use page in our index, we're gonna have to use page all the way down here, page.Data, and things, Data with a capital D. There we go. We're gonna have to do the whole page.Data with a capital D. Yeah, there we go, page.Data with capital D. We're gonna display with the page, fantastic.
[00:13:23]
All right, I think everything is looking nice right now. All right, so there we go. So we have the form, we have all that. Let's upgrade our templates as well because our index template currently doesn't actually pass the correct stuff around. So we'll just jump in here. And instead of passing the form, I'm gonna just do .Form, and for the display, I'm just gonna do .Data.
[00:13:43]
All right, kinda got those things through, things are nice. So now that means our form object here is that. All right, so that's pretty much everything that we're gonna need to do. We still kinda have some question marks around here. TODO, question mark, what do we do down here?
[00:13:55]
Well, let's first make sure we can trigger this and see that thing work. And if everything's gone good, look at that, we have that. If everything's gone good, we can refresh. Okay, we got some good stuff here. I'm gonna click this email, I should have made the email so much easier.
[00:14:08]
You know what? In fact, I'm gonna jump back here, I'm gonna go back to gd. Gosh dang, it's not even gd. Let's go like that. I'm gonna make it aoeu cuz I use Dvorak, by the way. By the way, I use Dvorak, and that's just my home row on this finger, makes it super easy to use.
[00:14:24]
So now that we have that there, I can just refresh that. Look at that. So now I should be giving a duplicate email, right? So when I create this, it should actually display a problem. What happened? Was check the network tab just to make sure we got the 400s.
[00:14:41]
The 400s has this, what the heck's going on? Well, several things. We didn't do several things correct, okay? First off, look at our targeting. Our targeting totally sucks, okay? We're not targeting the right stuff here. So let's just jump back and make sure we're targeting the right stuff.
[00:14:59]
We're gonna just take out the target. When you submit a form, its default target is itself. Okay, problem number one solved. Does that solve our problem? Well, of course not, it does not solve our problem. We have one other thing that's wrong, we responded with a 400. By default, HTMX is not going to display 400, right?
[00:15:21]
Because that would be really bad. You don't know what's coming down with your server. Second off, is 400 even the right status code? Technically, you should probably use something like 422. A 400 means that you've made a bad request. Did we make a bad request? No, semantically, we made a very correct request.
[00:15:36]
Here you go. We actually did a 422, the entity itself was unprocessable. So let's first do that. Let's change it to a 422 to be more semantically correct, even though now we're kinda getting into the pedantics, and some people are like, I don't like that. I do like this for this specific reason cuz it really allows HTMX to be the driver of your application state by having very proper status codes.
[00:15:58]
Cuz the second thing we're gonna do is we're gonna update and add this little script to our index. This little script, when we load the content, will add a body or an event listener to the body before swap. HTMX emits this event before it swaps out the content.
[00:16:15]
And what we're gonna do is say, hey, if this is a 422, we know this is good. So therefore, we should swap, and this is not an error. And so we're gonna tell HTMX on 422s we want to actually display what's going to happen because this is something we want.
[00:16:30]
And so this is another reason why using very precise, what's called status codes, allows you to actually have really precise behavior where you want it. And so this is kinda cool. This is kinda like a little cool, little thing right here. So I'm gonna jump over. Maybe some people don't think it's cool.
[00:16:45]
I think it's cool. So I'm gonna go down here right into my HTML, not even have it formatted well. We're just gonna put it in there. And now, when we refresh, we should be able to see this beautiful, beautiful, beautiful thing happen. So I'm gonna go aoeu, aoeu.
[00:16:59]
I'm gonna try to create contact, and it's gonna say, hey, contact already exists. We could add it, so I could throw in an autofocus here or a select here on the email to kinda make this thing happen well. We could do all the designing of the behavior you want your HTML to do in the backend.
[00:17:13]
Or you could really add it to the frontend if you were really, really excited about doing more JavaScript in your frontend. But at this point, we had a 422 response, and we are correctly identifying where to update stuff. Kinda cool, right? It's kind of exciting. I'm kind of excited.
[00:17:29]
I like it. I don't know, I think this is kinda fun cuz we're starting to break down problems in engineering paths. There's design patterns that are starting just to emerge, where you can handle forms in a very generalized way, at least in response to errors, and then how we're gonna drive in changes, okay.
[00:17:45]
I don't know, I kinda find this very exciting cuz I love the idea of being able to think of things in patterns, not how do you want to do it? How did the person design error handling in their form? Well, you look at any form, it's all gonna be unique.
[00:17:58]
It could be tons of different libraries that it's using. It could require JSON to some other library format. You're just doing a series of translations. Here, we're literally just going from server state into HTML. It just feels good, feels simple.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops