Search code examples
javascriptes6-promise

How to use a single default catch in a double returned promise?


Basically I have a construction like;

// Library function
function foo(){
  return new Promise((resolve, reject) =>{
  // Do lots of stuff, like rendering a prompt

  // On user action
  if(userDidSomething){
    resolve(user_input);
  }

  if(userCanceled){
     // On user cancel
     reject('User canceled');
  }
  }).catch("Default error (usually canceling whatever it is, which means; do nothing)");
}

// Function to call library function with predefined settings for this website
function bar(){
 // Have some defaults set here, like which kind of prompt it should be 
 return foo();
}

// Where the function will actually be used
function baz(){
 bar().then("do some things");
}

I've worked my way around this issue some years ago but unfortunately forgot how I actually managed to do that.

The goal: Have one standard catch handle things for me on the library level. If I want to overrule it, I can always do that later. So I guess it's: Break the chain.

The problem: Having the catch before the then, causes the then to be triggered once I have dealt with the catch (which is; ignoring it, in this case)

My current solution: I'm using throw on the library-level catch, this causes the promise to throw an uncaught error exception. However, this completely clutters up my console with errors which aren't really errors.

The problem is that the entire resolve/reject of the promise is being handled by the library. That promise gets returned around and I only call it way later.

This is a function I'm calling about 300 times throughout my project, and I don't want to be putting custom error handling on every single one of those function calls when the handling of this should be "don't do anything, really".


Solution

  • What you are asking for is not really possible using native Promises because that's not how they were intended to be used.

    You can, though, create a custom Promise class that does this. Note that muting all rejections is a bad idea because you won't see if your code errors out. To go around that, only a special value (SpecialPromise.CANCELED) is treated differently. The code has to track whether a .catch is attached to it. When the promise encounters the special value and it has no catch callback at the moment, it quickly attaches a no-op catch callback to silence the error:

    class SilentPromise extends Promise{
        constructor(executor){
            super((resolve, reject) => {
                executor(
                    resolve, 
                    e => {
                        if(e === this.constructor.CANCELED && !this._hasCatch){
                            this.catch(() => {})
                        }
                        reject(e)
                    }
                )
            })
            this._hasCatch = false
        }
        then(success, error){
            this._hasCatch = true
            return super.then(success, error)
        }
        catch(error){
            this._hasCatch = true
            return super.catch(error)
        }
        catchIfNotCanceled(error){
            this.catch(e => {
                if(e === this.constructor.CANCELED)
                    throw e
                return error(e)
            })
        }
    }
    
    SilentPromise[Symbol.species] = SilentPromise
    SilentPromise.CANCELED = Symbol('CANCELED')
    

    You can convert existing promises between SilentPromise and Promise using SilentPromise.resolve() and Promise.resolve().