TL;DR - If you've been in web development for the past few years, you may have heard the debate on error handling. "Use try / catch!", "No, use async / await / .catch!", "No, use promises / .catch!". Presented here are two new features that help developers clearly understand the connection status to a given IO socket and handle errors in a much cleaner, reactive way. With the plugin absorbing this responsibility, and now with developers having a completely new alternative and perspective on the issue at hand, hopefully the whole debate on error handling gets muted due to obsolescence of the underlying premise.
Disclaimer: I am the author nuxt-socket-io
Introduction
Unless you are Mr. Robot, who gets his code to work the first shot, you will most likely run into errors when your code attempts to request data from either your backend or some other service. Most likely, your code looks something like this:
try {
const resp = await Svc.getData({ userId: 'abc123' })
if (resp !== undefined) { // Note: Please don't do this.
// If it's undefined, it's an error if you were expecting a response.
/* handle response */
}
} catch (err) {
/* handle error */ // this placeholder comment stays here forever
throw new Error(err) // Note: Please don't do this!
// ^^ Don't catch an error just to throw it!)
}
Both blocks of code seem pretty simple and somewhat elegant, but the issue can quickly become a mess when you have many different kinds of requests to send. Your code will become littered with many try/catch blocks before you realize it. Considering that VueJS gives us reactive properties, and lets us create computed properties that change whenever other properties change, I think we can do better!
Here's my perspective. When I call some method to get data, these are my expectations:
// I want my request to be simple: (i.e., just make the request)
Svc.getData(...) // I just want to call this and have the response get sent directly to a property "resp".
// Success handling: (if all was good, handle response)
function handleResp(resp) { // If I want to post-process resp, I call this
/* handle resp */
// The response is valid here, if not...
// I have no business calling this function
}
// Error handling: (if errors occurred, collect them and don't set property "resp")
emitErrors: { // <-- send any errors directly to this property
getData: [{...}], // <-- send specific getData errors here
// it's useful to include hints and timestamps
}
This way, I can separate my concerns and keep my code completely organized. If emitErrors
becomes truthy, I can easily style different parts of the page or component based on that (using computed properties). Plus, if I can eliminate the need for validating the response inside of a handleResp
method, I also eliminated the need to have a test case for that. The time savings can seriously add up.
Connection Status
Many IO errors can be traced back to the actual connection to the service. Is the client even connected? This is the most fundamental question to ask, but easy to overlook. Fortunately, the socket.io-client exposes several events that the nuxt-socket-io plugin can listen for to determine the status if the user opts-in to listen (explained below). The following events are:
const clientEvts = [
'connect_error',
'connect_timeout',
'reconnect',
'reconnect_attempt',
'reconnecting',
'reconnect_error',
'reconnect_failed',
'ping',
'pong'
]
If it is desired to check the status, the user simply opts-in by defining the property socketStatus
on the same component that instantiates this.$nuxtSocket
. The plugin will then automatically set that status (it will use the camel-cased versions of the event names as prop names, since that's a common convention in Javascript). If it is desired to use a prop name other than socketStatus
, the ioOpts property statusProp
just needs to be set.
Examples:
data() {
return {
socketStatus: {}, // simply define this, and it will be populated with the status
badStatus: {} // Status will be populated here if "statusProp == 'badStatus'"
}
},
mounted() {
this.goodSocket = this.$nuxtSocket({
name: 'goodSocket',
channel: '/index',
reconnection: false
})
this.badSocket = this.$nuxtSocket({
name: 'badSocket',
channel: '/index',
reconnection: true,
statusProp: 'badStatus' // This will cause 'badStatus' prop to be populated
})
}
As a convenience to you, a SocketStatus.vue component is now also packaged with nuxt-socket-io, which will help visualize the status:
<socket-status :status="socketStatus"></socket-status>
<socket-status :status="badStatus"></socket-status>
Will produce the following dynamic tables:
So, with the socketStatus props being reactive, it makes it easy to show or hide parts of a given page based on the connection status.
Error Handling
Even when a connection is solid, it is still possible for IO errors to occur. Two main categories of errors can be thought of as: 1) timeout- and 2) non-timeout related. The plugin allows the user to take advantage of new built-in error handling features.
1) Handling timeout errors. It's possible for a timeout error to occur if the client is connected but makes an unsupported request (the request will just never get handled). The user opts-in to let the plugin handle timeout errors by specifying an emitTimeout
(ms) in the IO options when instantiating this.$nuxtSocket
:
this.socket = this.$nuxtSocket({ channel: '/examples', emitTimeout: 1000 }) // 1000 ms
Then, if an "emitTimeout" occurs, there are two possible outcomes. One is, the plugin's method will reject with an "emitTimeout" error, and it will be up to the user to catch the error downstream:
this.someEmitMethod()
.catch((err) => { // If method times out, catch the err
/* Handle err */
})
The above allows the user to write code in a manner that already feels familiar, however, I think there is an even easier way to deal with the error.
The plugin can provide a completely different way of handling an error, depending on whether the user allows it to or not. If the user defines a property "emitErrors" on the component and the server responds with an error attached (i.e., an object with the defined property "emitError"), the plugin won't throw an error, but will instead set the property on the component (this.emitErrors
) and organize this.emitErrors
by the faulty emit event. This may result in much cleaner code, and may make it easy to work with the component's computed properties that will change when "emitErrors" property changes:
data() {
emitErrors: {} // Emit errors will get collected here, if resp.emitError is defined
}
...
this.someEmitMethod() // Now, when this times out, emitErrors will get updated (i.e., an error won't be thrown)
Important NOTE: in order for this.emitErrors
to get updated, the server must send it's error response back as an object, and define a property "emitError". It is recommended for the backend to also attach error details to the response to aid with troubleshooting.
2) Handling non-timeout errors, such as bad requests, or anything specific to your application's backend. Again, like before, if this.emitErrors
is defined in the component, and the response is an object with a defined property "emitError", the property this.emitErrors
will get set on the component, otherwise, an "emitError" will get thrown. If it is desired to use a different name for the emitErrors prop, it is done so by specifying "emitErrorsProp" in the ioOptions:
data() {
myEmitErrors: {} // Emit errors will get collected here now
}
mounted() {
this.socket = this.$nuxtSocket({ emitErrorsProp: 'myEmitErrors' })
}
A Half-filled Promise
At the beginning of the article one of my first code snippets mentioned how I would want an empty to response to be considered as an error. This is still something I would like to consider, however, at the time of this writing, the plugin does not treat it as such. It only treats a defined resp.emitError
as a non-timeout error. I think it is safer for now for me to assume that not all users would want me to handle their empty responses for them, which is why I require them to opt-in in the manner described above. I would love it if enough people would want automated empty-response handling, but I first want to see how far people get with the code as-is before building more into it. Baby steps.
Conclusion
This article reviewed a completely different, and hopefully much simpler, way to deal with IO connection status and errors. When life seems to present us with only a few ways to solve a problem (try/catch vs. promise/catch), I like to think of yet another way to solve the problem with less effort, whenever possible. The plugin now includes that other way and I hope you find it helpful!
Top comments (2)
Hello
I am a developer who is developing using nuxt.
I'm asking because something difficult came up.
ConnectTimeout exists in statusProp, does this mean the time before reconnecting when socket connection fails?
How do I use the nuxt socket timeout?
I checked the data using statusProp, but I want to set the value of connectTimeout inside. I don't know how to set it up.
Please help me.
Hi, this article was written a while ago. The newest version of socket.io doesn't emit the "connectTimeout" event. Please try upgrading.