Ensuring ESHOPMAN Stability: Best Practices for Robust Redis Connection Handling

As an ESHOPMAN developer, ensuring the stability and reliability of your headless commerce application is paramount. The ESHOPMAN platform, built on Node.js/TypeScript, leverages powerful tools like Redis for its event bus and workflow engine. These components are crucial for seamless storefront management within HubSpot, handling everything from product updates via the Admin API to complex order processing workflows.

Recently, our community identified a critical issue related to how ESHOPMAN’s Redis loaders handle connection failures during application startup. This can lead to misleading logs and, more severely, unhandled rejections that can destabilize your ESHOPMAN instance and impact your HubSpot CMS storefront.

The Challenge: Misleading Logs and Unhandled Rejections During Redis Connection

The core of the problem lies in a specific pattern used by ESHOPMAN's event-bus-redis and workflow-engine-redis loaders when establishing a connection to Redis:

await new Promise(async (resolve) => {
  await connection.connect(resolve)
})

While seemingly innocuous, this pattern can lead to two significant issues when Redis is unreachable or experiences a connection timeout (e.g., connect ETIMEDOUT) during ESHOPMAN's boot sequence:

  • Misleading Success Logs: When connection.connect(resolve) fails, it passes the error as a value to the resolve callback. The outer Promise then resolves successfully, bypassing any surrounding try/catch blocks. Consequently, the ESHOPMAN loader incorrectly logs "Connection to Redis … established," even though the connection attempt failed.
  • Unhandled Rejections: The inner await connection.connect(...) still rejects with an error like Connection is closed.. Because nothing explicitly observes the promise returned by the async executor within the outer Promise, this rejection becomes "unhandled." In Node.js environments (version 15 and above), unhandled rejections can lead to process termination, causing unexpected downtime for your ESHOPMAN application and potentially disrupting your HubSpot CMS storefront.
  • Silent Errors: Without an explicit error listener on the Redis client, initial connection errors like ETIMEDOUT might only appear as generic [ioredis] Unhandled error event messages, making debugging more challenging.

Reproducing the Issue (Simplified)

To illustrate this behavior, a minimal reproduction using the underlying ioredis library demonstrates how a failed connection to a non-routable IP address can trigger these symptoms:

const IORedis = require("ioredis")
process.on("unhandledRejection", (e) => {
  console.log("UNHANDLED REJECTION:", e.stack.split("
").slice(0, 2).join("
"))
  process.exit(1)
})
const c IORedis("redis://10.255.255.1:6379", {
  maxRetriesPerRequest: null,
  enableReadyCheck: false,
  lazyConnect: true,
  connectTimeout: 500,
})
;(async () => {
  try {
    const r = await new Promise(async (resolve) => {
      await connection.connect(resolve)
    })
    console.log("loader went on, resolve() got:", r && r.message)
  } catch (err) {
    console.log("loader caught:", err.message) // never reached
  }
})()

This snippet would output both the [ioredis] Unhandled error event and the critical UNHANDLED REJECTION: Error: Connection is closed., confirming the problem.

The ESHOPMAN Solution: Robust Redis Connection Handling

The good news is that preventing these issues involves straightforward adjustments to the connection logic. The goal is to ensure that failed connections are properly caught by ESHOPMAN's own try/catch blocks and logged accurately, without generating unhandled rejections.

Here are the recommended approaches to fix the connection pattern:

  • Simplest Fix: Direct Await
    By simply awaiting the connect() method directly, the promise it returns will correctly reject on failure, allowing the surrounding try/catch to handle the error.
    await connection.connect()
  • Explicit Promise Rejection Handling
    If an explicit Promise wrapper is desired, ensure both resolution and rejection paths are handled correctly.
    await new Promise((resolve, reject) => connection.connect().then(resolve, reject))
  • Adding an Error Listener
    To prevent the generic [ioredis] Unhandled error event, it's best practice to add an error listener to your Redis client instance, logging errors through ESHOPMAN's module logger.
    connection.on("error", (err) => {
          // Log error using ESHOPMAN's logger
          // For example: ESHOPMAN_LOGGER.error("Redis connection error:", err);
        });

Ensuring ESHOPMAN Reliability

Implementing these robust connection handling practices is vital for any ESHOPMAN instance, especially for production environments where stability is critical for your headless commerce operations. By addressing these Redis connection nuances, you ensure that your ESHOPMAN application starts reliably, provides accurate logging, and avoids unexpected crashes, thus maintaining a smooth experience for your storefronts deployed via HubSpot CMS and efficient operations through the Admin API.

Stay tuned to the ESHOPMAN Migration Hub for more insights and best practices to optimize your e-commerce platform!

Start with the tools

Explore migration tools

See options, compare methods, and pick the path that fits your store.

Explore migration tools