0

In my Node app I am trying to get shipment data for packages. I need a way to get the json data and append it to an object or something so that I can pass it to my render page (using pug).

This is my code:

    var test;

    for(var i = 0; i < result.length; i++) {
        var currentNumber = result[i].trackingNumber;
        ups.track(currentNumber, function(err, tracking) {
            test += tracking
        });
    }

    res.send(result)

It seems that anything I do inside of the ups.track has no scope into the var test, and I can't think of a better way to do this. Any help would be appreciated.

jfriend00
  • 683,504
  • 96
  • 985
  • 979

2 Answers2

1

First off, ups.track() is an asynchronous function. That means that it will call its callback some indeterminate time in the future. So, your for loop runs to its completion, launching result.length calls to ups.track() and then you do res.send(result) before any of them have completed, before any of them have called their callback.

Secondly, you aren't initializing your test variable so test += ... won't produce a good result.

So, you can't know the results until all of them have completed. There are various ways to do that.

Low-tech Counter

The "low-tech" way to do that is to keep a counter:

var test = "";

var cntr = 0;
for(var i = 0; i < result.length; i++) {
    var currentNumber = result[i].trackingNumber;
    ups.track(currentNumber, function(err, tracking) {
        test += tracking;
        // see if we are done with all the ups.track() calls yet
        if (++cntr === result.length) {
            res.send(test);
        }
    });
}

Using Promises

The higher-tech way is to use promises (this has larger benefits to learn for lots of other reasons). While promises show only modest benefits here, they provide huge benefits as soon as you have sequences of asynchronous operations you need to coordinate and they also make error handling (something you are ignoring in your code example) much simpler too.

First, you make a "promisified" version of ups.track() which is a function that returns a promise that resolves or rejects when ups.track() finishes.

function upsTrack(num) {
    return new Promise(function(resolve, reject) {
        ups.track(num, function(err, tracking) {
           err ? reject(err) : resolve(tracking);
        });
    });
}

Now, we'll use a combination of .map() to create an array or promises from all your calls to upsTrack() and Promise.all() to tell us when all those promises have completed.

Promise.all(result.map(function(item) {
    return upsTrack(item.trackingNumber);
})).then(function(allResults) {
    // allResults is an array of tracking results
    // process that array into your final result and send it
    res.send(allResults.join(","));
}).catch(function(err) {
    // handle error here
    res.status(500).end();
});

Note that this structure also gets rid of multiple variables defined in the previous low-tech way of doing this as there is no need for test, cntr or i any more. Looping structures and functions that process arrays or handle arrays of results take care of all that for us.

Using the Bluebird Promise library

And, you can take even a few more shortcuts by using a smarter promise library like Bluebird:

const Promise = require('bluebird');
ups = Promise.promisifyAll(ups);

Promise.map(result, function(item) {
    return ups.trackAsync(item.trackingNumber);
}).then(function(allResults) {
    // allResults is an array of tracking results
    // process that array into your final result and send it
    res.send(allResults.join(","));
}).catch(function(err) {
    // handle error here
    res.status(500).end();
});

The Bluebird promise library has two useful things here. It has a promisifyAll() method which will make promisified methods of all the ups object methods for you automatically and it also has Promise.map() which just combines array.map() and Promise.all() in one step (since it's a commonly used structure).

If you're interested in Promises and Promise libraries, you may find this useful:

Are there still reasons to use promise libraries like Q or BlueBird now that we have ES6 promises?

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • See, here I would argue that your `if (err) return reject(err); resolve(tracking);` is far more disturbing, as regards a chosen style of codebase. You've got both an implicit `else` and an implicit `return`, are using an explicit `return` in the place of braces, and unlike the explicit `if` and `return`, the implicit `else` and `return` happen at different points in the sequence of statements. That's where I've seen even good devs write terrible bugs in, while extending or fixing other bugs, and not finding it for hours (or days), because of ambiguity of intent when refactoring. – Norguard Jul 09 '17 at 06:44
  • @Norguard - So you never write `if` statements that have a `return` in them? Really? This code is not hard to read at all. If there was more going on in, I'd use braces to block them out, but this is really simple so that's not required for readability or maintenance. But, I don't mind your ternary style for this situation so I modified mine to use that. – jfriend00 Jul 09 '17 at 06:57
  • I wouldn't say never. But I never mix tactics, outside of perhaps some guard-clauses on the top of a longer function. And I use braces (and semicolons), because in this case, brief kills. At least it has killed teams that I was on. If everyone was always rested, and this was the common style we were using for a project, we'd all grin and bear it, and update our linters and formatters to match. But I've seen these things go south in bigger refactors. – Norguard Jul 09 '17 at 08:25
0

The first problem is that test is undefined at the start. That's bad. You want a default value of the same type you expect it to be var test = "";

The big problem, though, is that you are sending before any of those ups callbacks ever return.

You've fired off 18 (or whatever) requests to UPS, but you've returned result before the UPS server even got the first request, let alone completed them all.

Rather than show you the really hairy code that might be written to make things work in your loops, I'd recommend trying something like this:

const track = trackingNumber =>
  new Promise((resolve, reject) =>
    ups.track(trackingNumber, (err, data) =>
      err ? reject(err) : resolve(data)));

Promise.all(result.map(info => track(info.trackingNumber)))
  .then(trackingNumbers => trackingNumbers.join(''))
  .then(bigStringOfNumbers => res.send(bigStringOfNumbers));

Learning about Promises, Promise.all(), and [].map() is going to be a much more helpful path than trying to build out the complex callback version of this solution.

Norguard
  • 26,167
  • 5
  • 41
  • 49
  • Do people really think that `const track = trackingNumber => ...` is a great way to define a function? If so, I'm disturbed at what Javascript has come to. Is that really the best readable code you can write? What happened to using the `function` keyword when you intend to declare a reusable function? – jfriend00 Jul 09 '17 at 05:37
  • @jfriend00 Function declarations are both hoisted and mutable. Function expressions have been used in code for a very, very long time (either as object properties or as variables; exported ES5 IIFE modules and configurable functions require it, if to be done in a pure way). If I were defining an `identity` function and a `noop` function, which style would be more imminently readable? The style where practically all of the characters have nothing to do with the context of the function, or where practically all of them do? – Norguard Jul 09 '17 at 05:43
  • I'm not objecting to the function expression (though it isn't my favorite). I'm objecting to the lack of the `function` keyword anywhere when defining a reusable function. You apparently think it is wasted space. I find it very useful in scanning the code and quickly recognizing what it is doing. I'm disappointed that people are choosing to entirely ditch the `function` keyword in their ES6 coding style. I think it had a very useful place in making code easier to read. Brevity is no particular goal of mine. I'd much rather have easily readable code than brief code. – jfriend00 Jul 09 '17 at 05:45
  • @jfriend00 in addendum, that function isn't necessarily reusable. It's a localized function, which is extracted so that its implementation doesn't need to be visible within the level of abstraction which the `Promise.all` code is working at. Something that Uncle Bob and Martin Fowler are proponents of, in any language. Additionally, the function, now localized, can be exported and tested (integration, not unit, due to its expectation of the `ups` service, but still). For reuse, the signature is more important than the body. And in Flow or TS, that's covered. Even just raw JS in VSCode. – Norguard Jul 09 '17 at 05:50
  • Also `function` tells me nothing, to be honest. My teams usually assume that all blocks are functions, and nothing runs by default. That's less to do with `brief` and more to do with the fact that we don't need to see the three separate `function` keywords and the three separate `return` statements there, which become actively detrimental when writing expressions that all do exactly one thing. Were I intending to illustrate a traditional codebase that looked like C or Java (or traditional JS), I'd be right behind you, in following those styles. – Norguard Jul 09 '17 at 05:51
  • As it stands, most of my codebases look monadic/monoidal, with the occasional `do` block, or applied lisp (with the function name on the outside) these days. Especially now that TS has improved generic handling in 2.4. – Norguard Jul 09 '17 at 05:58