1

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".

NoobishPro
  • 2,539
  • 1
  • 12
  • 23
  • *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)* - this is not true, order doesn't matter here – Konrad Aug 24 '22 at 16:45
  • Order does matter in a Promise. Once I let the catch end, it continues up the chain with the next .then(), as the catch was resolved. If I do `return "hello world!"` in my catch, my `.then(r => alert(r))` will say "Hello world!". That's kind of the whole issue. – NoobishPro Aug 24 '22 at 17:02
  • You are calling both `resolve` and `reject` you should call only one. – Konrad Aug 24 '22 at 17:14
  • no, I'm not calling any resolve. I'll clarify my question – NoobishPro Aug 24 '22 at 17:16
  • You are right, I was wrong – Konrad Aug 24 '22 at 17:31
  • I wonder what should happen with the promise in your goal. A promise can be either `fulfilled`, `rejected` or `pending`, no other state to choose from. – Konrad Aug 24 '22 at 17:50
  • So you basically want to return a promise, that, when rejected: 1. doesn't call `.then`s, 2. doesn't print the error to the console. Right? – FZs Aug 24 '22 at 18:03
  • @FZs Correct! Although a custom error like "canceled" would do, but now it throws an uncaught exception because I do a `throw` to break the chain. And if I do handle it without an uncaught exception, it resolves back into the `then` at the end of the chain. – NoobishPro Aug 24 '22 at 22:37
  • @KonradLinkowski yes, and I want it to get the status rejected, as keeping it on pending slowly eats RAM. Especially in an often recurring situation. As my question states, the problem is that once I reject it, but the chain is `catch().then()` instead of `then().catch()`, it continues into the 'then' after the catch. Handling the catch properly, turns the rejection into a resolve. – NoobishPro Aug 24 '22 at 22:39

1 Answers1

1

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().

NoobishPro
  • 2,539
  • 1
  • 12
  • 23
FZs
  • 16,581
  • 13
  • 41
  • 50
  • Hmm I want to try this ASAP. A silent promise is a great idea! What you're saying about the errors is definitely true, but this is specifically about canceling a row of prompts, which should yield no errors as the handling is "do nothing". I would only use this there. Anyway, what is the `Symbol` thing, exactly? I'm a bit confused about that one. I must admit to being a little confused about the use. – NoobishPro Aug 26 '22 at 09:44
  • @NoobishPro Well, a [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) is just a unique value. It is a normal JavaScript value that can be stored in variables and passed around like a number or a string, but similarly to how objects work, every call to `Symbol()` will return a new and unique symbol. They can also be keys in objects. – FZs Aug 26 '22 at 20:49
  • @NoobishPro The [`Symbol.species`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/species) line just makes chaining the `SilentPromise` class return other SilentPromises, not normal Promises. The next line creates the `SilentPromise.CANCELLED` value, and if you throw that in a SilentPromise, it will be silent. – FZs Aug 26 '22 at 20:51
  • 1
    Thank you, this is *exactly* what I needed! I can just call or use promises exactly as intended, but on the few exceptions (user cancel/hiding of the active elements popping up) I can just silently kill the process including the promise. I can switch any promise to this depending on the rejection type (it being cancelled) after the fact. It's absolutely perfect. I still don't fully understand it as much as I'd like (the species thing), but it works perfectly. Thank you so much! – NoobishPro Aug 27 '22 at 14:20
  • 1
    @NoobishPro You're welcome :) Setting a constructor's `Symbol.species` property specifies what constuctor that class' instance methods *should* use to create other instances (they don't have to, but all built-in types do use it). That is, setting it to `SilentPromise` is what made `SilentPromise.then()`, `SilentPromise.catch()`, etc. return other SilentPromises, not ordinary Promises (note that it's still `Promise.then` that's creating the new instance - SilentPromise call it with `super.then`). – FZs Aug 27 '22 at 15:14