Handling Publication Errors

What do you do when your server side Publication goes wrong? What if the data you want to publish is not available? What if the external API that you are relying on is down? How do we tell this to the user in a meaningful way? I was faced with this problem recently while working on my app.

When things go wrong on the client side it's usually quite straight forward. We detect the error (hopefully) and notify the user accordingly. But when things go wrong on the server side, then what?

In this article we will explore:

  • How to catch errors inside publications on the server.
  • How to forward errors to the client and catching them inside subscriptions.
  • The difference between synchronous and asynchronous error handling.
  • How to improve on general error messages with meaningful information.
  • Complimentary bonus towards the end ;)

Sync vs Async

In JavaScript Land we have both synchronous (blocking) functions and asynchronous (non-blocking) functions. They have slightly different execution flows as you probably know.

  • Asynchronous functions typically use callback functions to deliver the result, and you have probably come across them at least once.
  • Synchronous functions "block" the execution until they return the result.

They work a bit differently when it comes to error handling and we will get into more detail on that.

Catching Exceptions

You might be familiar with try and catch from other languages. In JavaScript we have that as well. If you're not familiar with the concept, here's a quick rundown. If you already know it, kindly skip to the next section.

From the MDN JavaScript Reference1 :

The try...catch statement marks a block of statements to try, and specifies a response, should an exception be thrown.

In other words:

  • You are calling one or more functions which are able to throw an Exception.
  • Exceptions are basically formalized Error events that can be thrown by a function to indicate an error.
  • By catching the Exception we can control the execution flow of the program.
  • If nothing went wrong, proceed as normal.

This is how you express it in code:

try {  
  // this function can throw an exception:
  someObject.doSomething();  
}
catch (Exception) {  
  // we caught an exception. now what?
}

Some things to note:

  • You may have an arbitrary number of statements within the try clause if you like.
  • You can have nested try / catch statements and can pass exceptions on from an inner to an outter catch clause. We will talk more about that in a bit.

Unexceptional errors

Exceptions are typically thrown by synchronous functions. What if we're calling asynchronous functions? As already mentioned, asynchronous functions use callbacks to deliver the results, usually together with an error parameter, for instance:

HTTP.get(url, options, function (error, response) {  
  // check for error 
});

We will explore this further but first, let's head over to the client and have a look at the subscription code.

Subscribing

In order to catch publication errors on the client side, we first need to set up our code properly. Consulting the excellent Meteor API docs2 we learn the following regarding Meteor.subscribe:

Meteor.subscribe(name, [arg1, arg2...], [callbacks])

...

callbacks Function or Object

Optional. May include onStop and onReady callbacks. If there is an error, it is passed as an argument to onStop. If a function is passed instead of an object, it is interpreted as an onReady callback.

Ok, that sounds handy! In fact the onStop callback receives a Meteor.Error. Let's quickly consult the Meteor docs again:

new Meteor.Error(error, [reason], [details])

As we can see, the Meteor.Error object has three properties: error, reason and details.

Let's see it in action; In our subscribe statement, let's add the callback object containing our onStop method as the last argument. Make sure to pass an object and not just a function as functions will be treated as an onReady callback only:

Meteor.subscribe('my-publication', {  
  onStop: function (e) {
    console.log("Error:", e.error);
    console.log("Reason:", e.reason);
    console.log("Details:", e.details);
  }
});

Now, if an Exception occurs in our publication code on the server, the following Meteor.Error gets logged on the client:

Error: 500  
Reason: Internal server error  
Details: undefined  

That's much better already. We're actually able to notify the user that something is wrong. The problem is we don't know what happened. Just that something happened. Wouldn't it be great if we could get some more detail?

Throwing

So, back to the problem. Let's say we're setting up a Meteor Publication on the server and the data we want to publish to the client is coming from an external source, such as a web API. All is going well until suddenly we're not receiving any data on the client. Here's a scenario:

Meteor.publish('my-publication', function() {  
  //Do a synchronous http request:
  const request = HTTP.get(url, options);
  ...
}

Upon examining the server logs we find these errors: Exception from sub my-publication id jGCuS25HgCiSvQxe8 Error: connect ECONNREFUSED

Seems like there's a problem connecting to the web API, but the client is just seeing a generalized Error 500 type error. It doesn't really tell anything about the problem. Let's fix that!

It's time to play catch, using what we've learned:

Meteor.publish('my-publication', function() {  
  //Do a synchronous http request:
  try {
    const request = HTTP.get(url, options);
  }
  catch (exception) {
    console.log("We caught an exception:", exception)
  }
  ...
}

If we run the app now we notice some things; In the server log we see this, just as we expected:

We caught an exception: { [Error: connect ECONNREFUSED]  
  code: 'ECONNREFUSED',
  errno: 'ECONNREFUSED',
  syscall: 'connect' }

Note the properties on the exception object above. These will come handy in providing details to the client.

But there's a problem. We're not getting any error message on the client anymore. Why is that?

Remember what I wrote about how you can have nested try / catch statements? What normally happens when we don't catch an exception in Meteor is that the exception "bubbles up" through the call stack until it reaches the global Meteor exception handler, which you might have guessed by now, is just another catch clause. That's the handler which causes the Error 500 message to be sent to the client.

Since we now have added our own try / catch statement, we have effectively created a nested exception handler, and since we already caught the exception, that's the end of the story. But to notify the client we need an exception, so how can we achieve that?

From the Meteor docs we learn the following about Meteor.Error:

If you want to return an error from a method, throw an exception. Methods can throw any kind of exception. But Meteor.Error is the only kind of error that a server will send to the client.

By simply using the expression below, we can create a new Meteor exception whenever we want, like so:

throw new Meteor.Error("My error message");  

By adding a throw inside the catch clause (also referred to as rethrowing) we notice that the client is now seeing the error again. This time with our own error description!

Let's rewind quickly: We saw before that Meteor.Error takes three arguments:

new Meteor.Error(error, [reason], [details])

...meaning that we can add a whole lot of information to our error if we like, and it will all be sent over the wire to the client. Let's do that!

Final snippet:

Meteor.publish('my-publication', function() {  
  //Do a synchronous http request:
  try {
    const request = HTTP.get(url, options);
  }
  catch (e) {
    //We caught an exception; Let's add some detail:
    throw new Meteor.Error(e.code, "API call failed", e.syscall);
  }
}

Great! We can now catch an exception, pick the information we want, and then create a new, augmented exception, which will be sent to the client. That's all good, but we're not done quite yet.

Calling back

All the catching and throwing is great, but it only works with synchronous stuff, right? So what about the asynchronous stuff...

It's actually easy because we don't have to wrap our code in try / catch statements, but there is a catch (no pun intended).

By simply using this.error() in our asynchronous callback (inside our publication) and passing a Meteor.Error as argument, we can notify the client in the exact same way as above.

Note here that this refers to our Meteor.publish scope. If you are not using ES2015 arrow functions3 (aka fat arrow functions) or any other means of binding the scope to this, you are probably using self or something similar instead of this in your code, so make sure you get that right. And that's the catch!

Example:

Meteor.publish('my-publication', function () {  
  //Do an asynchronous http request:
  HTTP.get(url, options, (err, res) => {
    if (err) {
      this.error(
        new Meteor.Error(err.code, "API call failed", err.syscall)
      );
    }
  });

Notice that we still use the Meteor.Error object to propagate our error to the client with the same arguments, just as before.

But wait, there is more!

Complimentary Method calls

In fact what we've learned so far is not exclusive to publications. You can throw errors from within Meteor.methods as well, using the same expression as above. Likewise, the error will be provided in the Meteor.call callback on the client side.

Example:

// Server (synchronous)
Meteor.methods({  
  postData: function() {
    try {
      const request = HTTP.post(url, options);
    }
    catch (e) {
      // throw a custom error message
      throw new Meteor.Error("POST request failed");
    }
  }
});
// Client
Meteor.call('postData', function(error, result) {  
  // check for error
});

I recommend reading up on the Methods4 chapter in the Meteor Guide for more in depth on the subject.

Wrapping it up

So there you have it. You now know how to:

  • Handle synchronous publication exceptions using try / catch.
  • Handle asynchronous publication errors using this.error().
  • Catch errors in client subscriptions using the onStop() callback function.
  • throw server exceptions from inside publications and methods that get sent to the client.
  • Use Meteor.Error properties to create a detailed error which can be used to inform the user in a meaningful way.

Hope you enjoyed reading this article. I learned a lot writing it. If you have comments or found errors - please let me know. You can find me on LinkedIn, The Meteor University Slack channel (apply here) or simply throw a message to forward at ricci dot se.

Until next time
Joel Ricci

References

Picture by Luis Llerena courtesy of Unsplash

Joel Ricci

Read more posts by this author.

Sweden / Bali

Subscribe to Meteor University

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!