Dealing with errors in Ember with Custom API Responses
Ember version used: 3.8 (LTS)
Backend: Rails
I started working with Ember recently and one of the things I've had to deal with is dealing with custom API responses. Ember usually expects incoming data to follow the JSON:API format like the one below:
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever.",
"created": "2015-05-22T14:56:29.000Z",
"updated": "2015-05-22T14:56:28.000Z"
},
"relationships": {
"author": {
"data": {"id": "42", "type": "people"}
}
}
}],
"included": [
{
"type": "people",
"id": "42",
"attributes": {
"name": "John",
"age": 80,
"gender": "male"
}
}
]
}
Note - for a lot of people who've not worked with JSON:API, the above JSON example would look strange. data
holds the document's primary data. relationships
key holds the id
and type
of the person that has an article (from the example). It allows a client link all included resource objects without having to make extra GET
requests. included
holds the attributes of the person. included
is used to hold an array of included resources. You can get more info on JSON:API here
Ember is really nice and understands that not everyone would prepare their data in this format and provides us with methods that we can use to convert our API responses to the format that Ember loves.
I recently had to deal with displaying errors in my Ember app when a record fails to get saved and it was pretty tricky as I couldn't find any clear way to do it using Ember's RestSerializer
. I'm going to be walking us through my implementation.
ExtractErrors (link)
import DS from 'ember-data';
import ApplicationSerializer from './application';
const { errorsHashToArray } = DS;
export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
extractErrors(store, typeClass, payload) {
payload.errors = errorsHashToArray(payload.errors[0]);
return this._super(...arguments)
}
});
Ember uses extractErrors
to extract model errors after a save()
fails. It normally expects your errors to be in the .errors
property of the payload object, so the JSON object you send in should have an errors
root property.
extractErrors
normally expects an array of objects in JSON:API format. We first make use of errorsHashToArray
to convert our incoming errors hash to an array of errors.
We make use of it to convert a response payload in this format:
{
"errors": [
{
"name": [
"can't be blank"
],
"description": [
"can't be blank"
]
}
]
}
To this:
{
[
{
title: "Invalid attribue",
detail: "can't be blank",
source: { pointer: "/data/attributes/name" }
},
{
title: "Invalid attribue",
detail: "can't be blank",
source: { pointer: "/data/attributes/description" }
}
]
}
After which we can then call this._super
, passing in the new array, which is then mapped to the relevant fields on the model.
Accessing the errors
You can access the returned errors from your route files. Our errors would be available on the model after the Promise is rejected and we can retrieve them when we want to.
newRecord.save()
.then(() => console.log("Yay! Works"))
.catch(() => {
this.currentModel.set('nameErrors', newRecord.get('errors.name')) // errors on the name attribute
this.currentModel.set('guestsErrors', newRecord.get('errors.guests')) // errors on the guests attribute
this.currentModel.set('descriptionErrors', newRecord.get('errors.description')) // errors on the description attribute
this.currentModel.set('startTimeErrors', newRecord.get('errors.startTime')) // errors on the startTime attribute
this.currentModel.set('endTimeErrors', newRecord.get('errors.endTime')) // errors on the endTime attribute
})
What we are doing above is using a get
to retrieve errors on a particular attribute and setting it as a property in the currentModel
object. (NB: I couldn't access model
so I went with the next best thing :smiles:).
Displaying them
The errors can then be accessed in your templates for your users to see just how good you are with this kind of things. (Revel in your glory as a 10x dev, more like 1/2x or 1/99x).
{{#each model.nameErrors as |error| }}
<div class="help is-danger">
<ul>
<li>{{capitalize-word error.attribute}} {{ error.message }}</li>
</ul>
</div>
{{/each}}
You can have more than one error on an attribute so you'd ideally want to loop over each and display them to the user. You could also concatenate all and display as one long message. Ember allows you to do so.
Resources
- https://davidtang.io/2016/01/09/handling-errors-with-ember-data.html
- http://api.emberjs.com/ember-data/3.10/classes/DS.Errors/methods
- https://api.emberjs.com/ember-data/3.8/classes/DS.RESTSerializer/methods/extractErrors?anchor=extractErrors
- https://api.emberjs.com/ember-data/3.10/classes/DS/methods?anchor=errorsHashToArray
Hope this tutorial was helpful. Expect more to come soon. Bye