how to use js Promise to eliminate pyramid of doom - javascript

i'm trying to understand how to use js Promise api to refractor a code that has lots of nested IF.
example when getting JSON object from localstorage a normal code would look like
function $storage(key,default) {
let json = localStorage.getItem(key);
if(json === null) return default;
try{ // <-- need try catch in case value was not valid json object
json = JSON.parse(json);
} catch (e) {
json = default;
}
return typeof json === 'object' ? json : default;
}
the readibility of this code is not that good. so i thought may be i can utilize js Promise to rewrite it into
function $storage (key, default) {
let ret;
let promise = new Promise( (y,n) => y(localStorage) )
.then( ls => JSON.parse(ls.getItem(key)) )
.then( json => typeof json === 'object' ? json : HOW_TO_THROW_ERROR() )
//on more validation step if needed
.then( json => typeof json === 'object' ? json : HOW_TO_THROW_ERROR() )
.then( valid_json => { return = valid_json } )
.catch( error => { ret = default; console.warn('json invalid',e); } );
return ret;
}
now i want to know how can i throw an exception inside then so that the catch can caught it and execute default ?
is this valid usage of js promise of am i wasting performance

You could use Promise.reject() to throw an error:
function $storage (key, default) {
let ret;
let promise = new Promise( (y,n) => y(localStorage) )
.then( ls => JSON.parse(ls.getItem(key)) )
.then( json => typeof json === 'object' ? json : Promise.reject("invalid json") )
.then( valid_json => { return = valid_json } )
.catch( err => { ret = default; console.warn(err.message); } );
return ret;
}
Although I find the following more legible and idiomatic.
function $storage(key,default) {
let json = localStorage.getItem(key);
if(json === null || typeof json !== 'object') json = default;
try{
json = JSON.parse(json);
} catch (e) {
json = default;
} finally {
return json
}
}
Promises are used, as you surely know, for asynchronous computation. Any other use might confuse other programmers.

You can use thrown to thrown the errors and then handle them in catch method
var p1 = new Promise(function(resolve, reject) {
resolve('Success');
});
p1.then(function(value) {
console.log(value); // "Success!"
throw 'oh, no!';
}).catch(function(e) {
console.log(e); // "oh, no!"
}).then(function(){
console.log('after a catch the chain is restored');
}, function () {
console.log('Not fired due to the catch');
});
// The following behaves the same as above
p1.then(function(value) {
console.log(value); // "Success!"
return Promise.reject('oh, no!');
}).catch(function(e) {
console.log(e); // "oh, no!"
}).then(function(){
console.log('after a catch the chain is restored');
}, function () {
console.log('Not fired due to the catch');
});
But if thrown some errors in async functions the catch is never called.
// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
setTimeout(function() {
throw 'Uncaught Exception!';
}, 1000);
});
p2.catch(function(e) {
console.log(e); // This is never called
});
source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch

in JavaScript you can use the keyword throw to throw any error.
Examples from MDN:
throw "Error2"; // generates an exception with a string value
throw 42; // generates an exception with the value 42
throw true; // generates an exception with the value true
throw new Error("Error");

function $storage (key, default) {
let ret;
let promise = new Promise( (y,n) => y(localStorage) )
.then( ls => JSON.parse(ls.getItem(key)) )
.then( json => typeof json === 'object' ? json : throw new Error("invalid json") )
//on more validation step if needed
.then( json => typeof json === 'object' ? json : throw new Error("invalid json") )
.then( valid_json => { return = valid_json } )
.catch( err => { ret = default; console.warn(err.message); } );
return ret;
}
You could basically just do the following, because if the parse fails, it will be catched automatically.
function $storage (key, default) {
let ret;
let promise = new Promise( (y,n) => y(localStorage) )
.then(ls => JSON.parse(ls.getItem(key)) )
.then(valid_json => { return = valid_json } )
.catch(err => { ret = default; console.warn(err.message); } );
return ret;
}

The problem I see is just about JSON.parse, wrapping it in a more usable function you get something like:
function safeParse(x) {
try {
return JSON.parse(x);
} catch(e) {
// Log the problem
return null;
}
}
function parmval(key, defval) {
var json = safeParse(localStorage.get(key));
return (typeof json === "object") ? json : defval;
}
Promises are about asynchronous operations, not IFs.

Related

Failing test - Mocha's done() called multiple times

I've tried looking at topics with a similar error but could not fit those solutions into the context of my issue.
When I try to run the the following test (function included that is tested):
function myFunc(next, obj) {
const pairs = {};
obj.listing.forEach((element) => {
if (element.x in pairs && pairs[element.x] !== element.y) {
const err = new Error('This was not ok');
next(err);
} else {
pairs[element.x] = element.y;
}
});
next();
}
it('should fail as 9 has been changed to 5 in the second object of the listing', function (done) {
const callback = (err) => {
if (err && err instanceof Error && err.message === 'This was not ok') {
// test passed, called with an Error arg
done();
} else {
// force fail the test, the `err` is not what we expect it to be
done(new Error('Assertion failed'));
}
}
myFunc(callback, {
"listing": [
{ "x": 5, "y": 9 },
{ "x": 5, "y": 11 }
]
});
});
I get this error:
What is the cause of this and how can I fix it?
You need to add a return in the if block of your myFunc so that the callback function next is called only once and indeed the done() callback in the main test case:
function myFunc(next, obj) {
const pairs = {};
obj.listing.forEach((element) => {
if (element.x in pairs && pairs[element.x] !== element.y) {
const err = new Error('This was not ok');
return next(err);
} else {
pairs[element.x] = element.y;
}
});
next();
}
#Ankif Agarwal's solution was not the correct one but it did point me in the right direction.
The forEach() method is not short circuited and therefor makes a call to next() more than once (Short circuit Array.forEach like calling break).
I was able to solve this in one of two way's.
By extracting the call to next() from the forEach() logic:
function myFunc(next, obj) {
const pairs = {};
let err = null;
obj.listing.forEach((element) => {
if (element.x in pairs && pairs[element.x] !== element.y) {
err = new Error('This was not ok');
} else {
pairs[element.x] = element.y;
}
});
if (err !== null) {
next(err);
} else {
next();
}
}
However this still makes the forEach() run through all element. If possible it seems better to short circuit it and break out of it soon as a violation occurs that sets the error, like so:
function myFunc(next, obj) {
const pairs = {};
const BreakException = {};
let err = null;
try {
obj.listing.forEach((element) => {
if (element.x in pairs && pairs[element.x] !== element.y) {
err = new Error('This was not ok');
throw BreakException;
} else {
pairs[element.x] = element.y;
}
});
next();
} catch (e) {
if (e !== BreakException) throw e;
next(err);
}
}
Hopefully someone can use this in the future.

how to throw error for destructuring non-existent key

So I'd like to destructure an object and have it throw an error if one of the keys didn't exist. I tried a try catch but it didn't work. And I'd like an alternative to just if (variable === undefined)
let obj = {test : "test"}
try {
let { test, asdf } = obj
console.log("worked")
}
catch (error) {
console.error(error)
}
Use proxy to throw the error if a non-existent property is fetched
let obj = {
test: "test"
};
try
{
let { test, asdf} = getProxy(obj);
console.log("worked");
}
catch (error)
{
console.error(error)
}
function getProxy(obj) {
return new Proxy(obj, { //create new Proxy object
get: function(obj, prop) { //define get trap
if( prop in obj )
{
return obj[prop];
}
else
{
throw new Error("No such property exists"); //throw error if prop doesn't exists
}
}
});
}
Demo
let obj = {
test: "test"
};
try
{
let { test, asdf} = getProxy(obj);
console.log("worked");
}
catch (error)
{
console.error(error)
}
function getProxy(obj) {
return new Proxy(obj, {
get: function(obj, prop) {
if( prop in obj )
{
return obj[prop];
}
else
{
throw new Error("No such property exists");
}
}
});
}
The try-catch work for the runtime errors, catching exceptions or error you throw explicitly. Thus, in destructuring, there is no such error thrown when the matching key is not found. To check the existence you need to explicitly create a check for it. Something like this,
let obj = {test : "test"}
let { test, asdf } = obj
if(test){
console.log('worked');
} else {
console.log('not worked');
}
if(asdf){
console.log('worked');
} else {
console.log('not worked');
}
This is because the destructuring works the same way as we assign the object value to another value like,
let obj = {test : "test"}
var test = obj.test;
var asdf = obj.asdf;
Here, doing obj.asdf will give you undefined in asdf and does not throw any exception. Thus, you cannot use try-catch for that.

Fluent async api with ES6 proxy javascript

So... I have some methods. Each method returns a promise.
myAsyncMethods: {
myNavigate () {
// Imagine this is returning a webdriverio promise
return new Promise(function(resolve){
setTimeout(resolve, 1000);
})
},
myClick () {
// Imagine this is returning a webdriverio promise
return new Promise(function(resolve){
setTimeout(resolve, 2000);
})
}
}
I'm trying to make end to end tests, so the prom chain must be linear (first click, next navigate, etc)
For now, I can do this...
makeItFluent(myAsyncMethods)
.myNavigate()
.myClick()
.then(() => myAsyncMethods.otherMethod())
.then(() => /*do other stuff*/ )
...with ES6 proxy feature:
function makeItFluent (actions) {
let prom = Promise.resolve();
const builder = new Proxy(actions, {
get (target, propKey) {
const origMethod = target[propKey];
return function continueBuilding (...args) {
// keep chaining promises
prom = prom.then(() => (typeof origMethod === 'function') && origMethod(...args));
// return an augmented promise with proxied object
return Object.assign(prom, builder);
};
}
});
return builder;
};
But, the thing I cannot do is the following:
makeItFluent(myAsyncMethods)
.myNavigate()
.myClick()
.then(() => myAsyncMethods.otherMethod())
.then(() => /*do other stuff*/ )
.myNavigate()
Because then is not a proxied method, and thus it does not return myAsyncMethods. I tried to proxy then but with no results.
Any idea?
thanks devs ;)
I would return wrapped Promises from yourAsyncMethods which allows mixing of sync and async methods with Proxy and Reflect and executing them in the correct order :
/* WRAP PROMISE */
let handlers;
const wrap = function (target) {
if (typeof target === 'object' && target && typeof target.then === 'function') {
// The target needs to be stored internally as a function, so that it can use
// the `apply` and `construct` handlers.
var targetFunc = function () { return target; };
targetFunc._promise_chain_cache = Object.create(null);
return new Proxy(targetFunc, handlers);
}
return target;
};
// original was written in TS > 2.5, you might need a polyfill :
if (typeof Reflect === 'undefined') {
require('harmony-reflect');
}
handlers = {
get: function (target, property) {
if (property === 'inspect') {
return function () { return '[chainable Promise]'; };
}
if (property === '_raw') {
return target();
}
if (typeof property === 'symbol') {
return target()[property];
}
// If the Promise itself has the property ('then', 'catch', etc.), return the
// property itself, bound to the target.
// However, wrap the result of calling this function.
// This allows wrappedPromise.then(something) to also be wrapped.
if (property in target()) {
const isFn = typeof target()[property] === 'function';
if (property !== 'constructor' && !property.startsWith('_') && isFn) {
return function () {
return wrap(target()[property].apply(target(), arguments));
};
}
return target()[property];
}
// If the property has a value in the cache, use that value.
if (Object.prototype.hasOwnProperty.call(target._promise_chain_cache, property)) {
return target._promise_chain_cache[property];
}
// If the Promise library allows synchronous inspection (bluebird, etc.),
// ensure that properties of resolved
// Promises are also resolved immediately.
const isValueFn = typeof target().value === 'function';
if (target().isFulfilled && target().isFulfilled() && isValueFn) {
return wrap(target().constructor.resolve(target().value()[property]));
}
// Otherwise, return a promise for that property.
// Store it in the cache so that subsequent references to that property
// will return the same promise.
target._promise_chain_cache[property] = wrap(target().then(function (result) {
if (result && (typeof result === 'object' || typeof result === 'function')) {
return wrap(result[property]);
}
const _p = `"${property}" of "${result}".`;
throw new TypeError(`Promise chain rejection: Cannot read property ${_p}`);
}));
return target._promise_chain_cache[property];
},
apply: function (target, thisArg, args) {
// If the wrapped Promise is called, return a Promise that calls the result
return wrap(target().constructor.all([target(), thisArg]).then(function (results) {
if (typeof results[0] === 'function') {
return wrap(Reflect.apply(results[0], results[1], args));
}
throw new TypeError(`Promise chain rejection: Attempted to call ${results[0]}` +
' which is not a function.');
}));
},
construct: function (target, args) {
return wrap(target().then(function (result) {
return wrap(Reflect.construct(result, args));
}));
}
};
// Make sure all other references to the proxied object refer to the promise itself,
// not the function wrapping it
Object.getOwnPropertyNames(Reflect).forEach(function (handler) {
handlers[handler] = handlers[handler] || function (target, arg1, arg2, arg3) {
return Reflect[handler](target(), arg1, arg2, arg3);
};
});
You would use it with your methods like
myAsyncMethods: {
myNavigate () {
// Imagine this is returning a webdriverio promise
var myPromise = new Promise(function(resolve){
setTimeout(resolve, 1000);
});
return wrap(myPromise)
},
// ...
Please note two things :
You might need a polyfill for Reflect : https://www.npmjs.com/package/harmony-reflect
We need to check proxy get handlers for built-in Symbols, e.g. : https://github.com/nodejs/node/issues/10731 (but also some browsers)
You can now mix it like
FOO.myNavigate().mySyncPropertyOrGetter.myClick().mySyncMethod().myNavigate() ...
https://michaelzanggl.com/articles/end-of-chain/
A promise is nothing more than a "thenable" (an object with a then() method), which conforms to the specs. And await is simply a wrapper around promises to provide cleaner, concise syntax.
class NiceClass {
promises = [];
doOne = () => {
this.promises.push(new Promise((resolve, reject) => {
this.one = 1;
resolve();
}));
return this;
}
doTwo = () => {
this.promises.push(new Promise((resolve, reject) => {
this.two = 2;
resolve();
}));
return this;
}
async then(resolve, reject) {
let results = await Promise.all(this.promises);
resolve(results);
}
build = () => {
return Promise.all(this.promises)
}
}
Them you can call it in both ways.
(async () => {
try {
let nice = new NiceClass();
let result = await nice
.doOne()
.doTwo();
console.log(nice);
let nice2 = new NiceClass();
let result2 = await nice2
.doOne()
.doTwo()
.build();
console.log(nice2, result2);
} catch(error) {
console.log('Promise error', error);
}
})();

Rejecting Promise if many error sources

Im in situation where I have many potential error sources. Is there an elegant solution to this mess?
How should I reject it?
function myFuction(hash) {
return new Promise((resolve, reject) => {
// this could return error
const id = atob(hash);
// this could return error
let data = firstFunction(id);
// return error if not true
if (data && data.id) {
// this could return error
return secondFunction(data.id)
.then(item => {
// return error if not true
if (item) {
// this could return error
return thirdFunction(item)
.then(payload => {
resolve('OK');
});
}
});
}
});
}
Avoid the Promise constructor antipattern! You can use early returns with Promise.reject or just throw errors:
function myFuction(hash) {
return Promise.resolve().then(() => {
// this could throw error
const id = atob(hash);
// this could throw error
let data = firstFunction(id);
// return error if not true
if (!data || !data.id)
return Promise.reject(new Error("…")); // alternative: throw new Error("…");
return secondFunction(data.id);
}).then(item => {
// return error if not true
if (!item)
return Promise.reject(new Error("…")); // same here
return thirdFunction(item);
}).then(payload => 'OK');
}
(Additionally I applied some flattening, but as long as your always return from promise callbacks you could nest as well)

How to catch error and continue executing a sequence in RxJs?

I have a list of items to parse, but the parsing of one of them can fail.
What is the "Rx-Way" to catch error but continue executing the sequence
Code Sample:
var observable = Rx.Observable.from([0,1,2,3,4,5])
.map(
function(value){
if(value == 3){
throw new Error("Value cannot be 3");
}
return value;
});
observable.subscribe(
function(value){
console.log("onNext " + value);
},
function(error){
console.log("Error: " + error.message);
},
function(){
console.log("Completed!");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.6/rx.all.js"></script>
What I want to do in a non-Rx-Way:
var items = [0,1,2,3,4,5];
for (var item in items){
try{
if(item == 3){
throw new Error("Value cannot be 3");
}
console.log(item);
}catch(error){
console.log("Error: " + error.message);
}
}
I would suggest that you use flatMap (now mergeMap in rxjs version 5) instead, which will let you collapse errors if you don't care about them. Effectively, you will create an inner Observable that can be swallowed if an error occurs. The advantage of this approach is that you can chain together operators and if an error occurs anywhere in the pipeline it will automatically get forwarded to the catch block.
const {from, iif, throwError, of, EMPTY} = rxjs;
const {map, flatMap, catchError} = rxjs.operators;
// A helper method to let us create arbitrary operators
const {pipe} = rxjs;
// Create an operator that will catch and squash errors
// This returns a function of the shape of Observable<T> => Observable<R>
const mapAndContinueOnError = pipe(
//This will get skipped if upstream throws an error
map(v => v * 2),
catchError(err => {
console.log("Caught Error, continuing")
//Return an empty Observable which gets collapsed in the output
return EMPTY;
})
)
const observable = from([0, 1, 2, 3, 4, 5]).pipe(
flatMap((value) =>
iif(() => value != 3,
of(value),
throwError(new Error("Value cannot be 3"))
).pipe(mapAndContinueOnError)
)
);
observable.subscribe(
(value) => console.log("onNext " + value), (error) => console.log("Error: " + error.message), () => console.log("Completed!")
);
<script src="https://unpkg.com/rxjs#7.0.0/dist/bundles/rxjs.umd.min.js"></script>
You need to switch to a new disposable stream, and if an error occurs within it will be disposed safely, and keep the original stream alive:
Rx.Observable.from([0,1,2,3,4,5])
.switchMap(value => {
// This is the disposable stream!
// Errors can safely occur in here without killing the original stream
return Rx.Observable.of(value)
.map(value => {
if (value === 3) {
throw new Error('Value cannot be 3');
}
return value;
})
.catch(error => {
// You can do some fancy stuff here with errors if you like
// Below we are just returning the error object to the outer stream
return Rx.Observable.of(error);
});
})
.map(value => {
if (value instanceof Error) {
// Maybe do some error handling here
return `Error: ${value.message}`;
}
return value;
})
.subscribe(
(x => console.log('Success', x)),
(x => console.log('Error', x)),
(() => console.log('Complete'))
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.1/Rx.min.js"></script>
More info on this technique in this blog post: The Quest for Meatballs: Continue RxJS Streams When Errors Occur
To keep your endlessObservable$ from dying you can put your failingObservable$ in a higher-order mapping operator (e.g. switchMap, concatMap, exhaustMap...) and swallow the error there by terminating the inner stream with an empty() observable returning no values.
Using RxJS 6:
endlessObservable$
.pipe(
switchMap(() => failingObservable$
.pipe(
catchError((err) => {
console.error(err);
return EMPTY;
})
)
)
);
In a case where you don't want or can't access the inner observable that causes the error, you can do something like this :
Using RxJS 7 :
const numbers$ = new Subject();
numbers$
.pipe(
tap(value => {
if (value === 3) {
throw new Error('Value cannot be 3');
}
}),
tap(value => {
console.log('Value:', value);
}),
catchError((err, caught) => {
console.log('Error:', err.message);
return caught;
})
)
.subscribe();
for (let n = 1; n <= 10; n++) {
numbers$.next(n);
}
What is interesting here is the "caught" argument in the catchError operator that can be returned.
https://rxjs.dev/api/operators/catchError
It only works when the source observable is Hot.
In my case, I use redux-observable and I wanted a way to handle my errors in a single place.
I came up with this :
const allEpics = combineEpics(
// all my epics
);
export const rootEpic = (action$, state$, dependencies) => {
return allEpics(action$, state$, dependencies).pipe(
catchError((err, caught) => {
if (err instanceof MyError) {
return concat(of(displayAlert(err.message)), caught);
}
throw err;
})
);
};
If any of my epic throw a "MyError" exception, It will be caught and another action will be dispatched.
You can actually use try/catch inside your map function to handle the error. Here is the code snippet
var source = Rx.Observable.from([0, 1, 2, 3, 4, 5])
.map(
function(value) {
try {
if (value === 3) {
throw new Error("Value cannot be 3");
}
return value;
} catch (error) {
console.log('I caught an error');
return undefined;
}
})
.filter(function(x) {
return x !== undefined; });
source.subscribe(
function(value) {
console.log("onNext " + value);
},
function(error) {
console.log("Error: " + error.message);
},
function() {
console.log("Completed!");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.6/rx.all.js"></script>

Categories

Resources