1

This function is used to wait for millis number of second.

function delay(millis: number) {
    return new Promise((resolve, reject) => {

        setTimeout(() => {
            resolve();
        }, millis);
    });

My goal is then to return the timeout out of this function?

const timeout = await delay(20000);

on click somewhere else, user bored of waiting

clearTimeout(timeout)
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
TSR
  • 17,242
  • 27
  • 93
  • 197

6 Answers6

3

Simply extend the Promise object you'll be sending:

function delay( millis ) {
  let timeout_id;
  let rejector;
  const prom = new Promise((resolve, reject) => {
    rejector = reject;
    timeout_id = setTimeout(() => {
      resolve();
    }, millis);
  });
  prom.abort = () => {
    clearTimeout( timeout_id );
    rejector( 'aborted' );
  };
  return prom;
}

const will_abort = delay( 2000 );
const will_not_abort = delay( 2000 );

will_abort
  .then( () => console.log( 'will_abort ended' ) )
  .catch( console.error );

will_not_abort
  .then( () => console.log( 'will_not_abort ended' ) )
  .catch( console.error );

setTimeout( () => will_abort.abort(), 1000 );
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Great solution, but remember to assign setTimeout to `timeout_id`. `timeout_id = setTimeout(resolve, millis);` – Henning Jan 08 '21 at 19:19
  • @Henning yes thanks for the heads up. Note that it's actually not *necessary* to clear the timeout here since a Promise can only be resolved or rejected once, but still, it's probably better this way, at least because it's useless to keep that function to execute. – Kaiido Jan 09 '21 at 08:54
1

You can return the resolve / reject along with the promise

function delay(millis) {
    let reolver;
    return [new Promise((resolve, reject) => {
        resolver = resolve
        x = setTimeout(() => {
            resolve();
        }, millis);
    }), resolver];
}  

const [sleep1, wake1] = delay(2000)
sleep1.then((x) => console.log(x || 'wakeup 1')) // Auto wake after 2 seconds

const [sleep2, wake2] = delay(2000)
sleep2.then((x) => console.log(x || 'wakeup 2'))
wake2('Custom Wakeup') // sleep2 cancelled will wake from wake2 call 
    
Ashish
  • 4,206
  • 16
  • 45
1

You can use an AbortSignal:

function delay(ms, signal) {
  return new Promise((resolve, reject) => {
    function done() {
      resolve();
      signal?.removeEventListener("abort", stop);
    }
    function stop() {
      reject(this.reason);
      clearTimeout(handle);
    }
    signal?.throwIfAborted();
    const handle = setTimeout(done, ms);
    signal?.addEventListener("abort", stop);
  });
}

Example:

const controller = new AbortController()
delay(9000, controller.signal).then(() => {
  console.log('Finished sleeping');
}, err => {
  if (!controller.signal.aborted) throw err;
  // alternatively:
  if (err.name != "AbortError") throw err;

  console.log('Cancelled sleep before it went over 9000')
});
button.onclick => () => {
  controller.abort();
};
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks for sharing, is this some new JS feature ? – TSR Dec 04 '22 at 03:24
  • @TSR It's not part of ECMAScript but of the Web APIs, however also supported by nodejs. It's been around for a while (2017 IIRC). – Bergi Dec 04 '22 at 03:40
0

You can return a function which cancels the timer (cancelTimer) along with the Promise object as an array from the delay function call.

Then use the cancelTimer to clear it if required and which would also reject the Promise:

function delay(millis) {
  let cancelTimer;
  const promise = new Promise((resolve, reject) => {
    const timeoutID = setTimeout(() => {
      resolve("done");
    }, millis);
    cancelTimer = () => {
      clearTimeout(timeoutID);
      reject("Promise cancelled");
    };
  });
  return [promise, cancelTimer];
}

//DEMO
let [promiseCancelled, cancelTimer] = delay(20000);
(async() => {
  try {
    console.log("Promise result never printed", await promiseCancelled);
  } catch (error) {
    console.error("Promise is rejected", error);
  }
})();
cancelTimer();

const [promise, _] = delay(2000);
(async() => {
  console.log("Promise result printed", await promise);
})();
Fullstack Guy
  • 16,368
  • 3
  • 29
  • 44
0

I recommend that you do not modify the promise by attaching an .abort method to it. Instead return two values from your delay function

  1. the delayed value
  2. a function that can be called to cancel the timeout

Critically, we do not see "x" logged to the console because it was canceled -

function useDelay (x = null, ms = 1000)
{ let t
  return [
    new Promise(r => t = setTimeout(r, ms, x)), // 1
    _ => clearTimeout(t)                        // 2
  ]
}

const [x, abortX] =
  useDelay("x", 5000)
  
const [y, abortY] =
  useDelay("y canceled x", 2000)
  
x.then(console.log, console.error)

y.then(console.log, console.error).finally(_ => abortX())

console.log("loading...")

// loading...
// y canceled x

interactive demo

Here's an interactive example that allows you to wait for a delayed value or abort it -

function useDelay (x = null, ms = 1000)
{ let t
  return [
    new Promise(r => t = setTimeout(r, ms, x)), // 1
    _ => clearTimeout(t)                        // 2
  ]
}

const [ input, output ] =
  document.querySelectorAll("input")
  
const [go, abort] =
  document.querySelectorAll("button")
  
let loading = false

function reset ()
{ loading = false
  input.value = ""
  go.disabled = false
  abort.disabled = true
}

function load ()
{ loading = true
  go.disabled = true
  abort.disabled = false
}

go.onclick = e => {
  if (loading) return
  load()
    
  const [delayedValue, abortValue] =
    useDelay(input.value, 3000)
    
  abort.onclick = _ => {
    abortValue()
    reset()
  }
  
  delayedValue
    .then(v => output.value = v)
    .catch(e => output.value = e.message)
    .finally(_ => reset())
}
code { color: dodgerblue; }
<input placeholder="enter a value..." />
<button>GO</button>
<button disabled>ABORT</button>
<input disabled placeholder="waiting for output..." />

<p>Click <code>GO</code> and the value will be transferred to the output in 5 seconds.</p>
<p>If you click <code>ABORT</code> the transfer will be canceled.</p>
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Any reasoning against modifying the Promise returned? Note that it's not the Promise's proto that's being modified, but only a new method attached to the one instance. Also, your code is creating for ever pending Promises, and any hooks on that Promise will also be forever pending. – Kaiido May 22 '20 at 04:53
0

Just use an AbortSignal:

/**
 * Sleep for the specified number of milliseconds, or until the abort
 * signal gets triggered.
 * 
 * @param ms {number}
 * @param signal {AbortSignal|undefined}
 */
const sleep = (ms, signal) =>
  new Promise<void>((ok, ng) => {
    /** @type {number} */
    let timeout

    const abortHandler = () => {
      clearTimeout(timeout)

      const aborted = new Error(`sleep aborted`)
      aborted.name = 'AbortError'

      ng(aborted)
    }
    signal?.addEventListener('abort', abortHandler)

    timeout = setTimeout(() => {
      signal?.removeEventListener('abort', abortHandler)
      ok()
    }, ms)
  })
> const a = new AbortController()
undefined
> const s = sleep(900000, a.signal)
undefined
> a.abort()
undefined
> await s
Uncaught AbortError: sleep aborted
    at AbortSignal.abortHandler
    at innerInvokeEventListeners
    at invokeEventListeners
    at dispatch
    at AbortSignal.dispatchEvent
    at AbortSignal.[[[signalAbort]]]
    at AbortController.abort
    at <anonymous>:2:3
>

If you prefer not to fail the promise, i.e. treat the abort signal as "skip sleep", simply replace the call to ng(aborted) with a call to ok().

jcayzac
  • 1,441
  • 1
  • 13
  • 26
  • Don't forget to immediately reject/resolve the promise if the signal is already `.aborted`, like in [my answer](https://stackoverflow.com/a/74671353/1048572) – Bergi Dec 03 '22 at 22:39