5. Error Handling

In every distributed system, the robustness of your application depends on its ability to recover gracefully from unexpected events. The AMPS client provides the building blocks necessary to ensure your application can recover from the kinds of errors and special events that may occur when using AMPS.

Disconnect Handling

Every distributed system will experience occasional disconnections between one or more nodes. The reliability of the overall system depends on an application’s ability to efficiently detect and recover from these disconnections. Using the AMPS JavaScript client’s disconnect handling, you can build powerful applications that are resilient in the face of connection failures and spurious disconnects. For additional reliability, you can also use the high availability functionality (discussed in the following sections), which provides both disconnect handling and features to help ensure that messages are reliably delivered.

The Client class, included with the AMPS JavaScript client, contains a disconnect handler and other features for building highly-available applications. The Client includes features for managing a list of failover servers, resuming subscriptions, republishing in-flight messages, and other functionality that is commonly needed for high availability. This section covers the use of a custom disconnect handler in the event that the behavior of the Client does not suit the needs of your application. 60East recommends using the provided disconnect handler unless there is specific behavior that does not meet your needs (for example, if your application does not want to reconnect to AMPS in the event of a disconnection).

tip The Client class in the JavaScript library combines functionality of both Client and HAClient classes of other AMPS client libraries.

AMPS disconnect handling gives you the ultimate in control and flexibility regarding how to respond to disconnects. Your application gets to specify exactly what happens when a disconnection occurs by supplying a function to Client.disconnectHandler() which is invoked whenever disconnect event happens.

Example 5.1 shows the basics:

/**
* This function will be called when the connection needs to be
* established/restored.
*/
const connectToAMPS = async () => {
  try {
    await client.connect('ws://localhost:9000/amps/json');
    // Successfully connected
  }
  catch (err) {
    // Can't establish connection
  }
};

// create a client object
const client = new Client('disconnect-handler-demo');

/*
* disconnectHandler() method is called to supply a function for use
* when AMPS detects a disconnect. At any time, this function may be
* called by AMPS to indicate that the client has disconnected from
* the server, and to allow your application to choose what to do
* about it. The application continues on to connect.
*/
client.disconnectHandler(
    /**
    * Our disconnect handler’s implementation begins here. In this
    * example, we simply try to reconnect to the original server
    * after a 5 second pause. Errors are likely to occur here,
    * therefore we must have disconnected for a reason, but Client
    * takes care of catching errors from our disconnect handler.
    * If an error occurs in our attempt to reconnect and an exception
    * is thrown by connect(), Client will catch it and absorb it,
    * passing it to the errorHandler(), if registered.
    */
    (client, error) => {
        // simple: just wait for 5 seconds and reconnect
        setTimeout(connectToAMPS, 5000);
    }
);

// We begin by connecting and subscribing
connectToAMPS();

Example 5.1: Supplying a Disconnect Handler

By creating a more advanced disconnect handler, you can implement logic to make your application even more robust. For example, imagine you have a group of AMPS servers configured for high availability – you could implement fail-over by simply trying the next server in the list until one is found (a simplified version of the behavior provided by the DefaultServerChooser class).

Example 5.2 shows a brief example.

/**
* Here our application is configured with an array of AMPS
* server URIs to choose from instead of a single URI. Our
* client is constructed and configured with the disconnect
* handler and an array of URIs that will be used in the
* connectToNextUri() method as explained below.
*/
let currentUri = 0;
const uris = [
  'wss://localhost:9000/amps/json',
  'wss://localhost:9100/amps/json',
  'wss://localhost:9200/amps/json'
];

const client = new Client('failover-demo');

/**
* In our disconnect handler, we invoke connectToNextUri(),
* with the next URI in our array of URIs and display
* a warning message. This simplistic handler never gives up,
* but in a typical implementation, you would likely stop
* attempting to reconnect at some point.
*/
client.disconnectHandler((client, error) => {
  console.log('Switching to the next URI...');

  currentUri = (currentUri + 1) % uris.length;
  connectToNextUri(uris[currentUri]);
});

/**
* This function is invoked by the disconnect handler,
* with a URI to connect.
*/
async function connectToNextUri(uri) {
  try {
    await client.connect(uri);
    // Successfully connected
  }
  catch (err) {
    // Can't establish connection
  }
}

// We begin by connecting to the first URI
connectToNextUri(uris[0]);

Example 5.2: Simple Client Failover Implementation

Using a Heartbeat to Detect Disconnection

The AMPS client includes a heartbeat feature to help applications detect disconnection from the server within a predictable amount of time. Without using a heartbeat, an application must rely on the environment to notify the application when a disconnect occurs. For applications that are simply receiving messages, it can be impossible to tell whether a socket is disconnected or whether there are simply no incoming messages for the client.

When you set a heartbeat, the AMPS client sends a heartbeat message to the AMPS server at a regular interval, and waits a specified amount of time for the response. If the operating system reports an error on send, or if the server does not respond within the specified amount of time, the AMPS client considers the server to be disconnected.

The AMPS client processes heartbeat messages asynchronously. If your application publishes messages in a loop (a synchronous operation), or the message handler is receiving significant amount of messages at the moment, the client may fail to respond to heartbeat messages in a timely manner and may be disconnected by the server.

// Create a new client and set the heartbeat interval to 5 sec
const client = new Client('heartbeat-demo').heartbeat(5);

Example 5.3: Using a Heartbeat to Detect Disconnection

Unhandled Errors

When using the asynchronous interface, errors can occur that are not thrown to the user. For example, when a message with invalid JSON data was received, the error occurs in the process of parsing its data inside of the AMPS JavaScript Client. Consider the following example:

const onMessage = message => console.log(message.data);
await client.subscribe(onMessage, 'pokes', '/Pokee LIKE ' + userId);

Example 5.4: Where do Errors go?

In this example, we set up a subscription to wait for messages on the pokes topic, whose Pokee tag begins with our user id. When messages arrive, we print a message out to the console.

Inside of the AMPS client, the client received an message that contains invalid JSON data that cannot be parsed. When the error occurs, there is no handler for it to be reported to, and by default it is ignored.

In applications where it is important to deal with every issue that occurs in using AMPS, you can set an error handler via Client.errorHandler() that receives these otherwise-unhandled errors and exceptions. Making the modifications shown in Example 5.5 to our previous example will allow those errors to be caught and handled. In this case we are simply printing those caught errors out to the console.

const onMessage = message => console.log(message.data);

// This time, let's add the error handler
client.errorHandler(err => console.error('Error occurred:', err));

await client.subscribe(onMessage, 'pokes', '/Pokee LIKE ' + userId);

Example 5.5: Error Handler

In this example we have added a call to client.errorHandler(), registering a simple function that writes the text of the error out to the console. If errors are thrown in the message handler, those errors are written to the console.