loading...
Cover image for Nuxt Socket.IO: Connection Status and Error Handling Made Easy

Nuxt Socket.IO: Connection Status and Error Handling Made Easy

richardeschloss profile image Richard Schloss ・6 min read

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:
Alt Text

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!

Posted on by:

richardeschloss profile

Richard Schloss

@richardeschloss

My goal is to be efficient and effective (#EnE) by writing less code that accomplishes more. Wannabe minimalist.

Discussion

markdown guide