Skip to content

Instantly share code, notes, and snippets.

@domenic
Created January 21, 2016 23:28
Show Gist options
  • Save domenic/8ed6048b187ee8f2ec75 to your computer and use it in GitHub Desktop.
Save domenic/8ed6048b187ee8f2ec75 to your computer and use it in GitHub Desktop.
How to subclass a promise
// ES6
class AngularPromise extends Promise {
constructor(executor) {
super((resolve, reject) => {
// before
return executor(resolve, reject);
});
// after
}
then(onFulfilled, onRejected) {
// before
const returnValue = super.then(onFulfilled, onRejected);
// after
return returnValue;
}
}
// ES5
function AngularPromise(executor) {
var p = new Promise(function (resolve, reject) {
// before
return executor(resolve, reject);
});
// after
p.__proto__ = AngularPromise.prototype;
return p;
}
AngularPromise.__proto__ = Promise;
AngularPromise.prototype.__proto__ = Promise.prototype;
AngularPromise.prototype.then = function then(onFulfilled, onRejected) {
// before
var returnValue = Promise.prototype.then.call(this, onFulfilled, onRejected);
// after
return returnValue;
}
@heecheon92
Copy link

This is what I came up with after a few hours of debugging and trial+error. It stores the call stack of the location where it is instantiated, allowing rejectWithError() to produce useful errors even when it is called from a parallel asynchronous process, e.g. an event handler.

export class DeferredPromise<T> extends Promise<T> {
  resolve: (value: T | PromiseLike<T>) => void;
  reject: (reason: T | Error) => void;

  initialCallStack: Error['stack'];

  constructor(executor: ConstructorParameters<typeof Promise<T>>[0] = () => {}) {
      let resolver: (value: T | PromiseLike<T>) => void;
      let rejector: (reason: T | Error) => void;

      super((resolve, reject) => {
          resolver = resolve;
          rejector = reject;
          return executor(resolve, reject);   // Promise magic: this line is unexplicably essential
      });

      this.resolve = resolver!;
      this.reject = rejector!;

      // store call stack for location where instance is created
      this.initialCallStack = Error().stack?.split('\n').slice(2).join('\n');
  }

  /** @throws error with amended call stack */
  rejectWithError(error: Error) {
    error.stack = [error.stack?.split('\n')[0], this.initialCallStack].join('\n');
    this.reject(error);
  }
}

You can use it like this:

const deferred = new DeferredPromise();

/* resolve */
deferred.resolve(value);
await deferred;

/* reject */
deferred.reject(Error(errorMessage));
await deferred; // throws Error(errorMessage) with current call stack

/* reject */
deferred.rejectWithError(Error(errorMessage));
await deferred; // throws Error(errorMessage) with amended call stack

/* reject with custom error type */
class CustomError extends Error {}
deferred.rejectWithError( new CustomError(errorMessage) );
await deferred; // throws CustomError(errorMessage) with amended call stack

Example use in my own project: deferred-promise.ts usage in badge-usb.ts > BadgeUSB._handlePacket()

It works like a magic. Thanks for your work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment