Managing errors across a complex asynchronous workflow

In this last recipe, we'll introduce one last concept to have a full view of the basic features made available by the JavaScript Hubs API: how to react to errors that occur during all the possible SignalR operational steps and states.

For some specific cases, SignalR offers a couple of native ways to deal with errors as follows:

  • We can supply a fail() callback to any JavaScript promise object that SignalR returns. This means that we can either use fail() on the promise object returned when starting a connection to intercept errors during the connection process, or we can use fail() on the promise object returned during the remote invocation of a Hub method. By hooking a callback with these fail() methods, we can be notified about the exceptions that occur during either the networking portion of a remote invocation or the server-side execution of a Hub method. We should observe that for errors occurring because of networking issues, SignalR has some built-in logic to perform some retries before giving up after a certain amount of time.
  • We can call the error() method on $.connection.hub in order to supply a callback to be invoked when an error occurs on the current connection. This handler traps both the connection startup exceptions and the method invocation exceptions; however, for those cases where the SignalR retry logic is in place, this handler always, and immediately, receives the originating error. In other words, if the retry logic succeeds at some point, the previously mentioned fail handlers would not be triggered but we would see the error handler executed in any case.

These scenarios are managed by SignalR out of the box, but there are other types of errors that are not directly handled and must be intercepted with the traditional try...catch blocks. Let's illustrate a sample scenario as follows:

  1. We successfully connected to a remote Hub.
  2. After that, for some reason, we lost our connection to the server.
  3. The retry attempts performed by SignalR have already been completed with no luck, and a timeout has expired after which no more retries are performed and the connection goes in a disconnected state.
  4. We try to call a Hub method anyway.

In this scenario, for each Hub method invocation at step 4, SignalR would throw an exception complaining that we have to start the connection first, and no error or fail handler would ever see this exception. So, we'd have to set up a try...catch block around the method call to handle it properly.

Let's get ready to illustrate these different cases with some code.

How to do it…

Let's perform the usual setup steps as follows:

  1. Add an OWIN Startup class bootstrapping SignalR with a call to app.MapSignalR(); inside its Configuration() method.
  2. Add a Hub-derived type, which we'll call EchoHub.

For more details about these steps, you can check the previous chapters.

Now, the following steps need to be performed to edit the code:

  1. We first need to make the EchoHub code look similar to the following:
    using System;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    [HubName("echo")]
    public class EchoHub : Hub
    {
        public string Say(string message)
        {
            if (new Random().Next(2) == 0)
                throw new ApplicationException("Doh!");
            return message;
        }
    }

    This version of the Say() method just returns the supplied message parameter similar to the Calling a server-side Hub method, but we have modified it to throw an exception every now and then.

  2. We then need to add an HTML page, which we'll call index.html.
  3. In the body of the page, we put a couple of buttons, some instructions to display, and a ul element in order to append messages that we want to show on the screen as shown in the following code:
    <button id="connect">Connect</button>
    <button id="say" style="visibility: hidden">
        Say hello
    </button>
    <p>
        Try stopping the web server while clicking buttons...
    </p>
    <ul id="messages"></ul>
  4. We move to the <head> section and add the usual JavaScript references as follows:
    <script src="Scripts/jquery-2.1.0.js"></script>
    <script src="Scripts/jquery.signalR-2.0.2.js"></script>
    <script src="/signalr/hubs"></script>
  5. Now, let's proceed step by step with the client-side code. We first add an empty script block as follows:
    <script>
    ...
    </script>
  6. In the block, we add a simple utility function to append text messages inside the ul element previously mentioned as shown in the following code:
    var print = function (message) {
        $('#messages').append($('<li/>').html(message));
    };
  7. Then as usual, we add a jQuery document ready call (using the shorthand version):
    $(function() {
    ...
    });
  8. In the document ready call, we first notify that the page is ready and then take a reference to the hub object and the echo hub, as we already did in the previous recipes. This is shown in the following code:
    print('Ready'),
    var hub  = $.connection.hub,
        echo = $.connection.echo;
  9. Here, let's add our first error handler by calling the error() method on the hub reference, as shown in the following code:
    hub.error(function(error) {
        print('An error occurred on the hub connection: ' +
            error);
    });

    The callback that we supplied will be invoked any time an error occurs while SignalR is in a valid state, as explained earlier. This will also receive any error causing SignalR to perform some retry attempts to recover from the problem; the error would be received regardless of the final outcome of the recovery process.

  10. We then handle the click event on the connect button, and in the handler we first print that we are connecting and then we hide the connect button itself, as shown in the following code:
    $('#connect').click(function () {
        print('Connecting...'),
        $('#connect').css('visibility', 'hidden'),
    
        ...
    });
  11. We are ready to connect using the following line of code:
    var started = hub.start();

    The start() call returns a JavaScript promise object that we'll use to set up both a function to be called back when the connection is ready (done()), and another function to be triggered when an error occurs (fail()).

  12. Let's fill the done() callback that we mentioned in the previous step. First we print a message saying that we are connected, then we show the button to invoke the Say() method, and finally we prepare an event handler for the click event. The following code depicts this:
    started.done(function() {
        print('Connected!'), 
        $('#say').css('visibility', 'visible'),
        $('#say').click(function () {
            print('Saying...'),
            ...
        });
    });
  13. We complete the body of the click handler that we just introduced with a call to the Say() method on the echo proxy. We know that such a method returns a promise object, where we handle the possible outcomes using both done() and fail(); because we want to stay on the safe side for any other client-side unhandled error, we surround everything with a try...catch block as shown in the following code:
    try {
        var call = echo.server.say("hello!");
    
        call.done(function(message) {
            print('Said: ' + message);
        });
        call.fail(function(error) {
            print('An error has occurred during the method
                call: ' + error);
        });
    } catch (error) {
        print('An general SignalR error has occurred somewhere
            between client and server: ' + error);
    };

    The content of the catch block is trivial, while the body of the try block is very similar to what we already did previously in this chapter, with only the addition of the fail handler to intercept the errors that occur during the remote method invocation. Here, the roles are well defined: the fail() callback will intercept any asynchronous error that occurs during the remote invocation, while the catch block will intercept any synchronous error that occurs on the client. In particular, a synchronous failure can occur if we try to invoke a remote method while the connection is in an invalid state.

  14. We're almost done. Only a fail handler for the connection promise is missing, which we add after the started.done() call:
    started.fail(function (error) {
        print('An error has occurred connecting: ' + error);
        $('#connect').css('visibility', 'visible'),
    });

    Here, we just print the reason for the error and display the connect button to allow the user to attempt again.

The best way to understand what's going on here is to load the index.html page and try it. You will first connect and then you'll be allowed to say hello, which sometimes will fail because of the random exception thrown on the server side.

I also recommend that you try performing the same operations while randomly disrupting the connection, which you can easily achieve by stopping and restarting the hosting web server (by default, it would be an instance of IIS Express). If you do this after loading the page but before connecting or after connecting but before calling the Say() method, you will see different error messages. If you keep trying to trigger remote calls, you will clearly notice that the error messages change depending on whether a connection time out has expired already or not (more about this in Chapter 6, Handling Connections).

How it works…

The internals of these mechanisms are pretty complex and are also influenced by the actual transport strategy used by SignalR. For the goal of this book, it should be enough to say that each of these strategies applies all the necessary checks, maintains the needed state, performs the required retries to keep things working, and, if it's the case, raises useful exceptions that highlight what went wrong.

There's more…

We've been digging quite deep into how errors are handled in such an asynchronous context, but in the following chapters we'll see even more tools that SignalR provides, in order to enhance how server-side errors are sent to clients and control the various possible states that a connection can go through. All of them are very useful to build complex applications where a high degree of fault tolerance is a requirement.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset