Promises in Meteor.methods

I’m not so used to promises, can someone tell me where I’m wrong?

Meteor.methods({
  sendVerificationLink: () => {
    try {
      if (Meteor.userId()) {
        const promise = function getLink() {
          return Meteor.user().services.email.verificationTokens[0].token;
        }
          .then((result) =>
            Email.send({
              SecureToken: "xxx",
              To: "xxx@yopmail.com",
              From: "xxx@gmail.com",
              Subject: "This is the subject",
              Body: result,
            }).catch((error) => console.log("error: ", error))
          )
          .catch((error) => console.log("error: ", error));
        return promise.await();
      } else {
        throw new Meteor.Error("not-logged-in", "You are not logged in.");
      }
    } catch (err) {
      return { isError: true, err };
    }
  },

Promises play no role in this instance. The code would be as simple as follows:

Meteor.methods({
  sendVerificationLink: () => {
    if (!Meteor.userId()) {
      throw new Meteor.Error("not-logged-in", "You are not logged in.");
    }
    const body = Meteor.user().services.email.verificationTokens[0].token;
    Email.send({
      SecureToken: "xxx",
      To: "xxx@yopmail.com",
      From: "xxx@gmail.com",
      Subject: "This is the subject",
      Body: body,
    });
  }
});

Note:

  • it would make sense to check whether the object hierarchy Meteor.user().services.email.verificationTokens[0].token exists, because if it doesn’t, you’ll get a TypeError. I would use lodash get or Meteor._get.
  • if an error is thrown, just let it fly. Returning something like { isError: true, err } is rather unusual. The convention is that the client callback should be invoked with an error, if there was one, rather than a result, which contains an error.
  • what however would make sense in every method is a selective conversion of any error other than Meteor.Error to Meteor.Error. For this purpose all of my methods are wrapped in a function with a try catch calling this function:
const rethrowAsMeteorError = error => {
  if (error instanceof Meteor.Error) {
    throw new Meteor.Error(error.error, error.reason, error.stack);
  }
  if (error instanceof Error) {
    if (error.name === "ClientError") {
      throw new Meteor.Error(error.error, {message: error.message, details: error.details}, error.stack);
    }
    throw new Meteor.Error(error.message, { actualErrorType: error.name }, error.stack);
  }
  if (error instanceof Object) {
    throw new Meteor.Error(error.name, error.message, error.stack);
  }
  // the assumption is that "error" is of a primitive type, such as string or number, etc.
  throw new Meteor.Error(error);
}

The problem at hand in Meteor is that if an error other than Meteor.Error is thrown in method, all the client receives is a Meteor.Error along the lines of “500 Server Error”, which is very inadequate.

2 Likes

Thanks, but i get following error on client:

Exception while simulating the effect of invoking ‘sendVerificationLink’ TypeError: Cannot read property ‘email’ of undefined
at MethodInvocation.sendVerificationLink…

and nothing is passed to body variable, so mail is not sent.

when I console.log(Meteor.user().services.email.verificationTokens[0].token) it shows value on server? but reports same error on client.

Yep. So maybe…

...
import _ from 'lodash';
...
const body = _.get(Meteor.user(), "services.email.verificationTokens[0].token");
if (!body) {
  throw new Meteor.Error("no token");
}
...
1 Like

Don’t forget we now have optional chaining syntax as well!

4 Likes

You’re right, I did forget that.

EDIT 2: (no need to set up anything for the chaining syntax to work in Meteor and vscode)

The line again with the new syntax is this:

const body = Meteor.user()?.services?.email?.verificationTokens?.[0]?.token;

(EDIT 3: array item access fixed, thanks @rjdavid, also see docs in MDN)

2 Likes

I don’t believe there should be any kind of setup.

You’re right, as it seems nothing needs to be added. This whole eslint stuff is black magic to me.

It’s the magic of Meteor and it’s default babel configuration.

2 Likes

Something we learned in optional chaining with the null object value error in production; this is how we must do optional chaining for arrays

const sample = email?.tokens?.[0]?.token;

4 Likes

Thanks, I missed that bit! I must say, however useful the optional chaining is, the resulting expression can be very ugly.

Too bad that no simple shortcut syntax was introduced to the effect of “ignore whatever in the following expression can go wrong, since everything is optional here”.

Then we could write instead of

const body = Meteor.user()?.services?.email?.verificationTokens?.[0]?.token;

…something like this:

const body = ?!Meteor.user().services.email.verificationTokens[0].token;

…assuming there was an operator ?!

Strange thing. I’ve tried both ‘lodash’ and chaining syntax and got “undefined” for body variable.
Then, I’ve made a publish with ‘services.email.verificationTokens’ field and subscribed on client. With that everything is working fine, but I don’t understand why I have to subscribe on client to make server method work correctly?

You have the method defined in shared code space. That is to say on both the client and the server. When Metoer methods are available in both, Meteor runs the one on the client as a simulation so that it can perform latency compensation. If you remove the code from the client, it will no longer run the simulation and this error should clear up.

3 Likes

Which makes more sense than adding lodash package IMO

1 Like