-
-
Save domenic/8ed6048b187ee8f2ec75 to your computer and use it in GitHub Desktop.
// 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; | |
} |
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()
You don't have to define constructor argument If you don't need the value returned by .then
method to be of your class instance.
Example:
class DeferredPromise extends Promise {
static get [Symbol.species]() {
return Promise;
}
constructor() {
let internalResolve = () => { };
let internalReject = () => { };
super((resolve, reject) => {
internalResolve = resolve;
internalReject = reject;
});
this.resolve = internalResolve;
this.reject = internalReject;
}
}
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 stackExample use in my own project: deferred-promise.ts usage in badge-usb.ts >
BadgeUSB._handlePacket()
It works like a magic. Thanks for your work.
I've written much cleaner version: timeout-promise