5

I am using Oboe.js to parse a really really large JSON file

const promises = [];
oboe('http://domain/my-file.js')
  .node('items.*', item => {
    // parseItem() returns a rejected Promise because of invalid JSON items
    promises.push(parseItem(item));
  })
  .done(() => {
    Promise.all(promises).then(() => {
      doSomething();
    });
  })

But my Browser console gets flooded with Uncaught (in promise). The same occurs if you write a promise in a setTimeout() like

const promises = [];
setTimeout(() => {
  promises.push(Promise.reject());
}, 500);
// some time in the future
Promise.all(promises);

What's really strange: modern browsers behave differently. In Firefox Developer Edition everything works without the error messages and in Chrome I get flooded with Uncaught (in promise). In Chrome you get the message instantly if you write Promise.reject(); without a catch. In Firefox and Safari nothing happens.

So what's the solution for this? Ignoring the message? I mean if this behavior is really in the official promise spec then promises in asynchronous code does not make really sense for me.

MatthewMartin
  • 32,326
  • 33
  • 105
  • 164
LongFlick
  • 1,129
  • 1
  • 12
  • 21
  • 1
    There are currently two camps of people with regards to es6 promises. One camp (chrome devs included) consider the way you are using promises to be a misuse. The behavior you are experiencing is not in the spec for promises despite there being several proposals for it because the other camp keeps pushing back saying use cases like yours are relevant. Unfortunately, some of the browsers added unhandled promise rejection logging despite the lack of spec and despite the push back against it. – Micah Zoltu Oct 20 '15 at 15:18
  • @MicahZoltu to be fair, the camps are divided even among chrome devs. – Benjamin Gruenbaum Oct 20 '15 at 15:26
  • @MicahZoltu thank you for clarification. At the moment it's really confusing which implementation will be the right one. – LongFlick Oct 21 '15 at 05:45

1 Answers1

2

Your Oboe issue is based on the fact Oboe streams JSON in, so we never know in advance how many promises there are so we can't attach the responsibility to Promise.all in advance. We can "promisify" Oboe to be able to return promises in its methods.

Typically, I'd use RxJS to automatically create a stream from the event emitter -and RxJS's methods can already return promises and then aggregate it. However - since I don't want a third party library here, and it has less teaching value - let's implement it ourselves:

function addMap(oboe) { 
  oboe.map = function(selector, mapper){
    var promises = [];
    return new Promise(function(resolve, reject){ // create a new promise
      oboe.node(selector, function(match){
        var result = mapper(match); // get result
        // signal that we're handling the rejection to make sure it's not handled.   
        result.catch(function(){});
        promises.push(result);
      });
      oboe.fail(reject); 
      oboe.done(function(){ resolve(promises); });
   });
  };
}

Which would let us do:

var o = oboe("foo");
addMap(o);
o.map("items.*", item => downloadItem(item)).then(result => {
   // handle result here
}); 

Your setTimeout issue is very contrived. The vast majority of people do not write code that looks like this in practice - in practice adding a error handler asynchronously is a pretty rare use case when not working against an API that forces you to do so (like the Oboe.js example).

What's really strange: modern browsers behave differently

This is because Firefox uses GC for detecting unhandled rejections and Chrome a timer. It's implementation detail - the only guarantee you'll have is that errors will not be logged if attached within a microtask (synchronously, or in a then that executes on the same turn).

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Thank you for the example. You can transfer it to any callback based APIs like in Node.js or an simple AJAX request: if you wrap a callback based API with a promise, and this callback gets called some time in the future and you have to reject the promise, you have the same issue. In my case I wrap Oboe with a promise and call a second callback based API and wrap this in a promise as well. My second promise gets rejected and then I get the error above. So whats the solution for this? Unfortunately I am very limited in the StackOverflow comment system so I can't write too much here. – LongFlick Oct 20 '15 at 14:32
  • Or: I don't can use promises that are rejected some time in the future and are handled with Promise.all(). The main issue here is that I don't have a direct catch() on my promise but therefore in my Promise.all() call. If I make the catch() on my inner promise it works, but I don't want that. I want to catch it in Promise.all(). I believe I don't get it. So whats the solution for APIs where I have promises that calls APIs that are also promises but have no catch() handler. Should they all have catch() handlers? I thought that Promise.all().catch() was exactly what i am looking for – LongFlick Oct 20 '15 at 14:50
  • @LongFlick `var p = Promise.reject(); p.catch(() => {}); /* note we didn't change p, it is the same promise, we just added a catch handler */; p.then(...); /* no more unhandled rejection report, we explicitly opted out */` – Benjamin Gruenbaum Oct 20 '15 at 15:10
  • Yes I know. But I want to handle / catch it in Promise.all().catch(). In your example this function never gets called. If I have an outer function that gets the result from Promise.all() it doesn't get notified about any errors. – LongFlick Oct 20 '15 at 15:45
  • 2
    @LongFlick yes it does... promises are immutable, `p.catch(..)` _forks_ the promise, the original is still rejected. `var p = Promise.reject(Error()); p.catch(function(){}); Promise.all([p]).catch(errs => console.log("This gets called"));` – Benjamin Gruenbaum Oct 20 '15 at 15:56
  • Ah, ok I oversight that. This makes my issue disappear. Thank you! – LongFlick Oct 21 '15 at 05:47