Can you add a .then to a promise after it's created? - javascript

Promises just baffle me.
I'm trying to make a mock data service to imitate axios.
My mock put call passes a targetUrl to _fetch which then sees if it's a valid url and either returns a new Promise with a delayed .resolve
const _returnResponse = (mockData, time = 0) => new Promise((resolve) => {
setTimeout(() => {
resolve(mockData);
}, time);
});
or a new Promise with a delayed .reject
const _returnError = (time = simulatedDelay) => {
const returnValue = new Promise(((resolve, reject) => {
setTimeout(() => {
reject(new Error('error'));
}, time);
}));
return returnValue;
};
but when I make my mock put call this returns a mock data that the calling method interprets as a success and console logs in its .then
put(target, putBody) {
const returnValue = _fetch(target, simulatedDelay)
returnValue.then(response => _console('PUT', target, response, putBody));
return returnValue;
},
But with an invalid target console logs an uncaught error
or this handles the error correctly, but console logs an undefined response
put(target, putBody) {
const returnValue = _fetch(target, simulatedDelay).then(response => _console('PUT', target, response, putBody));
return returnValue;
},
Here's the calling method:
saveStuff({ commit, state }, newStuff) {
//other code
return this.$mockAxios.put(url, putBody)
.then((response) => {
return response;
});
},
I feel like I'm completely missing something and I've researched this for hours and I'm still not getting it.

As a direct answer to the question: yes, you can add .then() to a promise after it's created.
Example:
const hi = new Promise((resolve, reject) => {
setTimeout(() => resolve('hello'), 2000);
});
hi.then(result => console.log(result));
As for promises baffling you, I would recommend (aside from more reading) just playing around a lot in your IDE with setTimeout. I know you're already using setTimeout in your mock, but strip it down further and just run the code in its own file, so that you control the whole environment. With lots of console.log('blah') to see the order and such. Also ensure you're just as familiar with callbacks, as promises are just syntactic sugar for callbacks. Try to read up on the JS event loop at the same time - it might provide some context if you know how and when a callback/promise executes.
Note that you can even add .then() after the callback has resolved or rejected. Hence the term "promise" - it's literally a promise that your .then() function will run. https://en.wikipedia.org/wiki/Promise_theory
const hi = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('one second');
resolve();
}, 1000);
});
setTimeout(() => {
hi.then(() => console.log('two seconds. this executes approximately a full second after the first promise has resolved'));
}, 2000);

Related

How to use the AbortController to cancel Promises in React?

I want to cancel a promise in my React application using the AbortController and unfortunately the abort event is not recognized so that I cannot react to it.
My setup looks like this:
WrapperComponent.tsx: Here I'm creating the AbortController and pass the signal to my method calculateSomeStuff that returns a Promise. The controller I'm passing to my Table component as a prop.
export const WrapperComponent = () => {
const controller = new AbortController();
const signal = abortController.signal;
// This function gets called in my useEffect
// I'm passing signal to the method calculateSomeStuff
const doSomeStuff = (file: any): void => {
calculateSomeStuff(signal, file)
.then((hash) => {
// do some stuff
})
.catch((error) => {
// throw error
});
};
return (<Table controller={controller} />)
}
The calculateSomeStuff method looks like this:
export const calculateSomeStuff = async (signal, file): Promise<any> => {
if (signal.aborted) {
console.log('signal.aborted', signal.aborted);
return Promise.reject(new DOMException('Aborted', 'AbortError'));
}
for (let i = 0; i <= 10; i++) {
// do some stuff
}
const secret = 'ojefbgwovwevwrf';
return new Promise((resolve, reject) => {
console.log('Promise Started');
resolve(secret);
signal.addEventListener('abort', () => {
console.log('Aborted');
reject(new DOMException('Aborted', 'AbortError'));
});
});
};
Within my Table component I call the abort() method like this:
export const Table = ({controller}) => {
const handleAbort = ( fileName: string) => {
controller.abort();
};
return (
<Button
onClick={() => handleAbort()}
/>
);
}
What am I doing wrong here? My console.logs are not visible and the signal is never set to true after calling the handleAbort handler.
Based off your code, there are a few corrections to make:
Don't return new Promise() inside an async function
You use new Promise if you're taking something event-based but naturally asynchronous, and wrap it into a Promise. Examples:
setTimeout
Web Worker messages
FileReader events
But in an async function, your return value will already be converted to a promise. Rejections will automatically be converted to exceptions you can catch with try/catch. Example:
async function MyAsyncFunction(): Promise<number> {
try {
const value1 = await functionThatReturnsPromise(); // unwraps promise
const value2 = await anotherPromiseReturner(); // unwraps promise
if (problem)
throw new Error('I throw, caller gets a promise that is eventually rejected')
return value1 + value2; // I return a value, caller gets a promise that is eventually resolved
} catch(e) {
// rejected promise and other errors caught here
console.error(e);
throw e; // rethrow to caller
}
}
The caller will get a promise right away, but it won't be resolved until the code hits the return statement or a throw.
What if you have work that needs to be wrapped with a Promise constructor, and you want to do it from an async function? Put the Promise constructor in a separate, non-async function. Then await the non-async function from the async function.
function wrapSomeApi() {
return new Promise(...);
}
async function myAsyncFunction() {
await wrapSomeApi();
}
When using new Promise(...), the promise must be returned before the work is done
Your code should roughly follow this pattern:
function MyAsyncWrapper() {
return new Promise((resolve, reject) => {
const workDoer = new WorkDoer();
workDoer.on('done', result => resolve(result));
workDoer.on('error', error => reject(error));
// exits right away while work completes in background
})
}
You almost never want to use Promise.resolve(value) or Promise.reject(error). Those are only for cases where you have an interface that needs a promise but you already have the value.
AbortController is for fetch only
The folks that run TC39 have been trying to figure out cancellation for a while, but right now there's no official cancellation API.
AbortController is accepted by fetch for cancelling HTTP requests, and that is useful. But it's not meant for cancelling regular old work.
Luckily, you can do it yourself. Everything with async/await is a co-routine, there's no pre-emptive multitasking where you can abort a thread or force a rejection. Instead, you can create a simple token object and pass it to your long running async function:
const token = { cancelled: false };
await doLongRunningTask(params, token);
To do the cancellation, just change the value of cancelled.
someElement.on('click', () => token.cancelled = true);
Long running work usually involves some kind of loop. Just check the token in the loop, and exit the loop if it's cancelled
async function doLongRunningTask(params: string, token: { cancelled: boolean }) {
for (const task of workToDo()) {
if (token.cancelled)
throw new Error('task got cancelled');
await task.doStep();
}
}
Since you're using react, you need token to be the same reference between renders. So, you can use the useRef hook for this:
function useCancelToken() {
const token = useRef({ cancelled: false });
const cancel = () => token.current.cancelled = true;
return [token.current, cancel];
}
const [token, cancel] = useCancelToken();
// ...
return <>
<button onClick={ () => doLongRunningTask(token) }>Start work</button>
<button onClick={ () => cancel() }>Cancel</button>
</>;
hash-wasm is only semi-async
You mentioned you were using hash-wasm. This library looks async, as all its APIs return promises. But in reality, it's only await-ing on the WASM loader. That gets cached after the first run, and after that all the calculations are synchronous.
Async code that doesn't actually await doesn't have any benefits. It will not pause to unblock the thread.
So how can you let your code breath if you've got CPU intensive code like what hash-wasm uses? You can do your work in increments, and schedule those increments with setTimeout:
for (const step of stepsToDo) {
if (token.cancelled)
throw new Error('task got cancelled');
// schedule the step to run ASAP, but let other events process first
await new Promise(resolve => setTimeout(resolve, 0));
const chunk = await loadChunk();
updateHash(chunk);
}
(Note that I'm using a Promise constructor here, but awaiting immediately instead of returning it)
The technique above will run slower than just doing the task. But by yielding the thread, stuff like React updates can execute without an awkward hang.
If you really need performance, check out Web Workers, which let you do CPU-heavy work off-thread so it doesn't block the main thread. Libraries like workerize can help you convert async functions to run in a worker.
That's everything I have for now, I'm sorry for writing a novel
I can suggest my library (use-async-effect2) for managing the cancellation of asynchronous tasks/promises.
Here is a simple demo with nested async function cancellation:
import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";
// just for testing
const factorialAsync = CPromise.promisify(function* (n) {
console.log(`factorialAsync::${n}`);
yield CPromise.delay(500);
return n != 1 ? n * (yield factorialAsync(n - 1)) : 1;
});
function TestComponent({ url, timeout }) {
const [text, setText] = useState("");
const myTask = useAsyncCallback(
function* (n) {
for (let i = 0; i <= 5; i++) {
setText(`Working...${i}`);
yield CPromise.delay(500);
}
setText(`Calculating Factorial of ${n}`);
const factorial = yield factorialAsync(n);
setText(`Done! Factorial=${factorial}`);
},
{ cancelPrevious: true }
);
return (
<div>
<div>{text}</div>
<button onClick={() => myTask(15)}>
Run task
</button>
<button onClick={myTask.cancel}>
Cancel task
</button>
</div>
);
}

sync function that await an event (once more

Desperately trying to write a sync version of https://www.npmjs.com/package/node-firebird#reading-blobs-aasynchronous
Basically I need to (a)wait twice:
for the callback function to execute so that the eventEmitter is available
for the "end" event to occur
and then return the Buffer.
my code (JS/TS mix for now) currently does 2, but not 1 : readBlob returns undefined, then Buffer.concat(buffers) is called later ... :
function readBLOB(callback: any): Buffer {
return callback(async (err, _, eventEmitter) => {
let buffers = []
if (err)
throw err
eventEmitter.on('data', chunk => {
buffers.push(chunk);
});
return await eventEmitter.once('end', function (e) {
return Buffer.concat(buffers)
})
})
}
Sorry to ask one more time (yes, I checked a lot of other questions and tried a lot of things...), but how to make this work (simply...) ?
(the function that calls the callback is fetch_blob_async in https://github.com/hgourvest/node-firebird/blob/master/lib/index.js#L4261 , just in case...)
There are few mistakes here like returning an callback function, witch returns, i guess, undefined or returning something IN an callback function that makes no sense.
Also async / await makes no sense here it has no effect. async / await is only useful if you want to await till some Promise resolves. But you have no Promise in your code at all.
What you need is new Promise
function readBLOB(callback) {
return new Promise((resolve, reject) => {
callback((err, _, eventEmitter) => {
let buffers = [];
if (err) reject(err);
eventEmitter.on("data", chunk => {
buffers.push(chunk);
});
eventEmitter.once("end", function(e) {
resolve(Buffer.concat(buffers));
});
});
});
}
Simple like that. You resolve your Buffer and reject if some error occurs
Now you can use it like:
readBLOB(cb).then(data => {
console.log(data);
})

Does an async await Promise resolve require .then() to be called?

Let's say I have the following async function. The console.log will only fire after it has been resolved.
(async () => {
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
console.log('resolved');
})();
Is this valid JavaScript? Or should I always use .then()? What are the drawbacks or behavioral differences from using .then() here?
(async () => {
return await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
})().then(() => {
console.log('resolved');
});
Off-topic: I also noticed the latest method return await can be changed to just return without the results changing, but this thread answered that question.
Being able to handle asynchronous operations without always resorting to then is the main point of async/await. The first example is perfectly valid.
The second one is a bit of an antipattern as it would be functionally identical without the async/await for two reasons - marking your function with async implicitly makes it return a Promise, but you also explicitly return one - and awaiting the Promise causes the code to wait until the Promise resolves before returning it, but since you are chaining with then, the then doesn't run before the Promise resolves anyway.
This would be functionally identical:
(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
})().then(() => {
console.log('resolved');
});
The main drawback of using "synchronous-like" code like the first example is error handling - if you do 5 await operations in a row and any of them reject, your whole async function returns a rejected Promise. If you chain then operations, you can also insert catch handlers to handle specific error cases with more precision.

Propagating rejection from outside promise to inner promise

I have created a long running Promise that I'm wrapping with this simple function that I created to create a watch a Promise race.
The function is below:
export const promiseTimeout = (
promise,
timeoutMs = 10000, //10 secs
message = 'Timeout reached, please try again',
) =>
Promise.race([
promise,
new Promise((resolve, reject) =>
setTimeout(() => {
reject(message);
}, timeoutMs),
),
]);
The way I'm planning to use it is that I would pass the long running Promise that might require other unpredictable resources such as internet, file, system setting, etc.
Usage would be like below:
const result = await promiseTimeout(longRunningFunction())
.catch(err => /* do something with the error , show toast or alert */);;
What is currently happening with this is that whenever the timeout is reached it would call the catch but the operation of the longRunningFunction will still continue.
How can I stop the operations on the passed Promise in argument if ever timeout is reached?
How can I stop the operations on the passed Promise in argument if ever timeout is reached?
Hey, sorry, we don't have cancellation of async functions yet.
Note however that a promise is a value and not an action, once you have the promise given we won't have cancellable promises in JavaScript it is impossible to cancel the action.
The only thing you can do is to do something like the cancellation proposal and write your longRunningFunction with a token:
function longRunningFunction() {
const signal = { requested: false };
async function internal() {
// your regular code here
// whenever you can stop execution:
if(signal.requested) {
return; // and cancel internal operations
}
}
let res = internal();
res.signal = signal;
return res;
}
Then write your race as:
export const promiseTimeout = (
promise,
timeoutMs = 10000, //10 secs
message = 'Timeout reached, please try again',
) =>
Promise.race([
promise,
new Promise((resolve, reject) =>
setTimeout(() => {
reject(message);
if (promise.signal) promise.signal.requested = true;
}, timeoutMs),
),
]);

Chai 'expect' doesn't execute inside a callback

I'm writing unit-tests for my function that fetches info from some REST API. I am using ramda Future type (source).
The following test works weird:
it('should return Maybe of Nothing', done => {
let response = {
status: 200,
json: () => {
return {
results: []
}
}
}
let fakeFetch = {
fetch: () => {
return new Promise((resolve, reject) => {
resolve(response)
})
}
}
// String -> Future Error Maybe
let result = Utils.fetchGiantBomb(faker.random.word(), fakeFetch.fetch);
result.fork(err => {
assert.fail(err, 'expected to return Maybe of Nothing');
done();
}, data => {
expect(Maybe.isJust(data)).to.be.true;
done();
})
})
data should be of type Maybe.Nothing. If I expect Maybe.isNothing the test passes, but I want to see what happens when the test fails, so I set it to Maybe.isJust, which return false. After looking at this for a while, I noticed that when the expect fail it jump up to the error handling (err callback), which then just stop executing any assertion (which result in a 2000ms timeout).
In the Future sources I saw that when the success callback fails, it executes the failure callback. How can I complete this test so it display that the data is not what I expect?
I think the problem is that, when your REST call fails, done() is never called.
Not sure if expect has a .catch method when it fails, but you can try to add
.catch(done);
at the end of your expect function.
Hope that helps.
Calling future.fork(errorHandler, successHandler) will currently ensure that any exceptions thrown in the successHandler will propagate through to the errorHandler.
One way around this (though perhaps not ideal as it is undocumented) is to call future._fork(errorHandler, successHandler) rather than future.fork where errors thrown in the successHandler will not be captured.
Alternatively, a number of test frameworks support passing an error to the done callback such as:
result.fork(err => {
done('Expected to return Maybe of Nothing: ' + err);
}, data => {
expect(Maybe.isJust(data)).to.be.true;
done();
})
I think Ramda shouldn't catch the exception there. But i don't know what's they are trying to do.
It look like you are using Mocha. It maybe good idea to convert your Future to Promise first, then observe the Promise. ie:
const futureToPromise = future => {
return new Promise((resolve, reject) => future.fork(reject, resolve))
}
it('should return Maybe of Nothing', () => {
let response = {
status: 200,
json: () => {
return {
results: []
}
}
}
let fakeFetch = {
fetch: () => {
return new Promise((resolve, reject) => {
resolve(response)
})
}
}
// String -> Future Error Maybe
let result = Utils.fetchGiantBomb(faker.random.word(), fakeFetch.fetch);
// return it because Mocha can handle this
return futureToPromise(result).then(data => {
expect(Maybe.isJust(data)).to.be.true;
}, () => {
// fail
assert.fail(err, 'expected to return Maybe of Nothing');
})
})

Categories