[Feature Request]: Don't Throw Errors When `limiter.consume()` Rejects a Request from a User
First off, thanks so much for all of your efforts on this project, including the articles that you've written on Medium. It's all been really helpful for me as I've been trying build out a small app. This GitHub Issue is about something that could potentially improve developer experience.

## The Problem
Currently, the fact that `limiter.consume()` throws the `RateLimiterRes` object when a user can no longer make requests to the server prevents developers from optimizing their code. Consider the example for [rate limiting Login Pages based on User + IP](https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection). Oftentimes, the `user.exists` check will be an asynchronous function, because a lookup needs to be done in the database to verify whether or not the submitted `username`/`email` is already present. **This means that if the username lookup is slow enough, it's possible for `limiterSlowBruteByIP.consume(ipAddr)` to reject _before_ the `Promise.all` line is reached.** I'm in a Remix application in `node@18`, and the server crashes because of this unhandled rejection.

Re-throwing the error in a `catch()` callback isn't a viable option here because it will still result in the same issue: a `Promise` being rejected _before_ `await Promise.all()` unwraps the array of promises. Developers have two options to circumvent this problem:
1) Assign the rejected value to an external variable from within a `catch()` callback, and check to see whether or not that external variable was actually assigned to.
   - (This technically allows the developer to optimize their asynchronous code. But it's way too messy, especially if more than two rate limiters are being used.)
2) `await` each individual call to `limiter.consume()`.
   - This makes the code much easier to maintain, but it also makes the server less performant.

## The Solution

For now, the aforementioned workarounds are technically viable. But neither of them are ideal.

Developers would be able to accomplish much more (without any drawbacks) if the `limiter.consume()` method simply returned the `RateLimiterRes` object instead of throwing it. The object could have a `boolean` property (e.g., `obj.limitReached`) added to it. This property would let the developer know what they should do next. And it gives them control over whether they want to throw the error or not. This would be a _super_ helpful feature to have in this project (though it would technically be a breaking change).

## Example Migrations

If this idea seems worthwhile, here are some examples of what migrations could look like when upgrading to the new version.

The simplest migration is for developers to throw errors on their own. This would enable them to change the least amount of code.

```ts
try {
  // For single results
  rateLimter.consume(key).then((obj) => { if (obj.limitReached) throw obj });

  // For arrays of results
  await Promise.all(rateLimiterResArray).then((arr) => {
    const failedObj = arr.find((obj) => obj.limitReached);
    if (failedObj) throw failedObj;
  );
} catch (errorOrObject) {
  // ... Continue as normal
}
```

The other migration option is to remove the try/catch block and check the `boolean` property directly in the original code. (Hopefully this would make the code easier to read since all related logic would be in the same place instead of being separated across different blocks. However, this migration option requires slightly more effort.)

```ts
// No wrapping `try/catch` block needed. All relevant code/logic exists side-by-side. (Developer can still `throw` if they want to.)

// For single results
const singleObject = await rateLimiter.consume(key);
if (singleObject.limitReached) // Do something...

// For arrays of results
const arrayOfObjects = await Promise.all(rateLimiterResArray);
const failedObject = arrayOfObjects.find((obj) => obj.limitReached);
if (failedObject) // Do something...
```