18

I'm trying to gracefully handle redis errors, in order to bypass the error and do something else instead, instead of crashing my app.

But so far, I couldn't just catch the exception thrown by ioredis, which bypasses my try/catch and terminates the current process. This current behaviour doesn't allow me to gracefully handle the error and in order to fetch the data from an alternative system (instead of redis).

import { createLogger } from '@unly/utils-simple-logger';
import Redis from 'ioredis';
import epsagon from './epsagon';

const logger = createLogger({
  label: 'Redis client',
});

/**
 * Creates a redis client
 *
 * @param url Url of the redis client, must contain the port number and be of the form "localhost:6379"
 * @param password Password of the redis client
 * @param maxRetriesPerRequest By default, all pending commands will be flushed with an error every 20 retry attempts.
 *          That makes sure commands won't wait forever when the connection is down.
 *          Set to null to disable this behavior, and every command will wait forever until the connection is alive again.
 * @return {Redis}
 */
export const getClient = (url = process.env.REDIS_URL, password = process.env.REDIS_PASSWORD, maxRetriesPerRequest = 20) => {
  const client = new Redis(`redis://${url}`, {
    password,
    showFriendlyErrorStack: true, // See https://github.com/luin/ioredis#error-handling
    lazyConnect: true, // XXX Don't attempt to connect when initializing the client, in order to properly handle connection failure on a use-case basis
    maxRetriesPerRequest,
  });

  client.on('connect', function () {
    logger.info('Connected to redis instance');
  });

  client.on('ready', function () {
    logger.info('Redis instance is ready (data loaded from disk)');
  });

  // Handles redis connection temporarily going down without app crashing
  // If an error is handled here, then redis will attempt to retry the request based on maxRetriesPerRequest
  client.on('error', function (e) {
    logger.error(`Error connecting to redis: "${e}"`);
    epsagon.setError(e);

    if (e.message === 'ERR invalid password') {
      logger.error(`Fatal error occurred "${e.message}". Stopping server.`);
      throw e; // Fatal error, don't attempt to fix
    }
  });

  return client;
};

I'm simulating a bad password/url in order to see how redis reacts when misconfigured. I've set lazyConnect to true in order to handle errors on the caller.

But, when I define the url as localhoste:6379 (instead of localhost:6379), I get the following error:

server 2019-08-10T19:44:00.926Z [Redis client] error:  Error connecting to redis: "Error: getaddrinfo ENOTFOUND localhoste localhoste:6379"
(x 20)
server 2019-08-10T19:44:11.450Z [Read cache] error:  Reached the max retries per request limit (which is 20). Refer to "maxRetriesPerRequest" option for details.

Here is my code:

  // Fetch a potential query result for the given query, if it exists in the cache already
  let cachedItem;

  try {
    cachedItem = await redisClient.get(queryString); // This emit an error on the redis client, because it fails to connect (that's intended, to test the behaviour)
  } catch (e) {
    logger.error(e); // It never goes there, as the error isn't "thrown", but rather "emitted" and handled by redis its own way
    epsagon.setError(e);
  }

  // If the query is cached, return the results from the cache
  if (cachedItem) {
    // return item
  } else {} // fetch from another endpoint (fallback backup)

My understanding is that redis errors are handled through client.emit('error', error), which is async and the callee doesn't throw an error, which doesn't allow the caller to handle errors using try/catch.

Should redis errors be handled in a very particular way? Isn't it possible to catch them as we usually do with most errors?

Also, it seems redis retries 20 times to connect (by default) before throwing a fatal exception (process is stopped). But I'd like to handle any exception and deal with it my own way.

I've tested the redis client behaviour by providing bad connection data, which makes it impossible to connect as there is no redis instance available at that url, my goal is to ultimately catch all kinds of redis errors and handle them gracefully.

Vadorequest
  • 16,593
  • 24
  • 118
  • 215
  • "couldn't just catch the exception to do so, it always crashes." - what do you mean by "crashes"? Where do you see the `[Redis client] error` bits logged? Have you read the [Quick start](https://github.com/luin/ioredis#auto-reconnect)? – Nickolay Aug 11 '19 at 02:41
  • This log is from the `error` event and it gets logged in my AWS CloudWatch logs. By "crash", I mean the current process is terminated from redis when the connection cannot be created, I'd like to catch that error instead, I currently have no control over it and it breaks the workflow I'm trying to build, because it bypasses my try/catch attempt to gracefully handle the error. – Vadorequest Aug 11 '19 at 06:33
  • Do you mean it "crashes" by quitting via `throw e; // Fatal error, don't attempt to fix`? My reading of the docs suggest that connection errors, such as the "invalid password" error you're using, are first reported as an `error` event on the client object, and only after `maxRetriesPerRequest` attempts will the pending commands "be flushed with an error". You stop your program on the first error, so you don't get to that point. I think if you remove your code to quit on first error, you'll get your error in the `// It never goes there, as the error isn't "thrown"` block. – Nickolay Aug 11 '19 at 06:59
  • Thank you @Nickolay, I got lost in my tests and you helped me figuring it out. Indeed, there must not be any `throw` in the `client:on:error` event, I probably encountered some race conditions when testing, or fixed a bug since then, but it works fine that way now! – Vadorequest Aug 11 '19 at 12:57
  • 1
    Cool! I posted it as an answer then. – Nickolay Aug 11 '19 at 13:24
  • even without throw in my client.on("error") code, my nodejs application crashed. – Aung Myint Thein Jan 28 '21 at 14:07
  • how was the graceful error catching happens for you? @Vadorequest – Aung Myint Thein Jan 29 '21 at 02:27
  • @AungMyintThein It's been a while, but everything I mentioned above has been open-sourced at https://github.com/UnlyEd/GraphCMS-cache-boilerplate/blob/master/src/utils/redis.js, I suggest you take a look at the whole implementation, as it handles errors gracefully. See the README to understand what the project does :) – Vadorequest Jan 31 '21 at 11:21

2 Answers2

14

Connection errors are reported as an error event on the client Redis object.

According to the "Auto-reconnect" section of the docs, ioredis will automatically try to reconnect when the connection to Redis is lost (or, presumably, unable to be established in the first place). Only after maxRetriesPerRequest attempts will the pending commands "be flushed with an error", i.e. get to the catch here:

  try {
    cachedItem = await redisClient.get(queryString); // This emit an error on the redis client, because it fails to connect (that's intended, to test the behaviour)
  } catch (e) {
    logger.error(e); // It never goes there, as the error isn't "thrown", but rather "emitted" and handled by redis its own way
    epsagon.setError(e);
  }

Since you stop your program on the first error:

  client.on('error', function (e) {
    // ...
    if (e.message === 'ERR invalid password') {
      logger.error(`Fatal error occurred "${e.message}". Stopping server.`);
      throw e; // Fatal error, don't attempt to fix

...the retries and the subsequent "flushing with an error" never get the chance to run.

Ignore the errors in client.on('error', and you should get the error returned from await redisClient.get().

Nickolay
  • 31,095
  • 13
  • 107
  • 185
  • 1
    It did not work for me. Connecting to a cluster, the only way it would throw the error is by returning "null" on the clusterRetryStrategy: clusterRetryStrategy: (times: number) => { return null;}. Let me know if anyone has suggestions with clusters. Thank you. – user906573 Jan 07 '21 at 23:56
10

Here is what my team has done with IORedis in a TypeScript project:

  let redis;
  const redisConfig: Redis.RedisOptions = {
    port: parseInt(process.env.REDIS_PORT, 10),
    host: process.env.REDIS_HOST,
    autoResubscribe: false,
    lazyConnect: true,
    maxRetriesPerRequest: 0, // <-- this seems to prevent retries and allow for try/catch
  };

  try {

    redis = new Redis(redisConfig);

    const infoString = await redis.info();
    console.log(infoString)

  } catch (err) {

    console.log(chalk.red('Redis Connection Failure '.padEnd(80, 'X')));
    console.log(err);
    console.log(chalk.red(' Redis Connection Failure'.padStart(80, 'X')));
    // do nothing

  } finally {
    await redis.disconnect();
  }
PaulMest
  • 12,925
  • 7
  • 53
  • 50
  • did you put this in a function? are u connecting the redis every time a request is made? I am still stuck with unhandled error – Aung Myint Thein Jan 29 '21 at 04:24
  • @AungMyintThein set `enable_offline_queue` to false and read this and the linked questions https://stackoverflow.com/questions/66893022/how-to-configure-node-redis-client-to-throw-errors-immediately-when-connection – Stupid Man Mar 31 '21 at 20:57
  • Thanks, the explicit `disconnect()` prevented the infinite retries. Continued to work even after removing params: `lazyConnect`, `maxRetriesPerRequest`. – OXiGEN Oct 16 '22 at 02:17