Catch Error Thrown from setTimeout

Here's how to catch errors thrown from inside a setTimeout callback.

Uncaught Error

Errors thrown within setTimeout are especially squirrely. This is because the callback function is asynchronously. Explained by Node docs:

This will not work because the callback function passed to fs.readFile() is called asynchronously. By the time the callback has been called, the surrounding code (including the try { } catch (err) { } block will have already exited.

So, can you catch an error thrown from within setTimeout at all? Yes, but with constraints, and it's easy to do it wrong. If you don't catch it properly, it'll yield an uncaught exception error, and your program is likely to crash. For instance, this won't work:

try {
  setTimeout(() => {
    throw new Error('up')
  }, 1)
} catch (err) {
  console.error('Will it catch?', err) // no!
}

Here are a few options that will work:

Move the catch Inside the setTimeout

To use a try-catch with a thrown error, you'll need to catch inside the setTimeout callback:

setTimeout(() => {
  try {
    throw new Error('up')
  } catch (err) {
    console.error('Will it catch?', err) // yes!
  }
}, 1)

Reject a Promise

All of these samples are contrived. Here's another one: If you're using setTimeout, you're doing async programming, and you probably have Promises involved already. Instead of throwing, you could reject instead (or at least first). That will be catchable:

new Promise((_, reject) => {
  setTimeout(() => {
    reject(new Error('up'))
  }, 1)
}).catch((err) => {
  console.error('Will it catch?', err) // yes!
  // throw err // if desired
});

Return a Result

Another alternative: If thrown errors don't fit your use case super well -- say, for changing program flow based on an error state -- perhaps you'll want to avoid thrown errors and use something else. A nice abstraction for this is a Result, which will contain either the success or failure state.

As a type, a Result looks like this in TypeScript:

type Result<T, E = Error> = 
  { ok: true, value: T }
  { ok: false, error: E }

Instead of throwing, return an error-state Result:

async function doStuff() {
  return new Promise(resolve => {
    setTimeout(() => {
      return resolve({ ok: false, error: new Error('up') })
    }, 1)
  })
}

const result = await doStuff()
if (!result.ok)
  console.log('Can I get error feedback?', result.error) // yes!

Other options? Other pitfalls? Surely, surely. Let me know what you think.