Async function returning Promise Pending with Airtable data

I’m having trouble with my Node.js application that connects to Airtable. I keep getting Promise Pending instead of actual data.

My setup:

  • Working with two Airtable bases: employees and positions
  • Trying to match employee records with their position details
  • Using async/await but something is wrong with my promise handling

The issue happens when I try to fetch records and merge data from both tables. My async function doesn’t seem to wait properly for all the data to load before returning.

router.get('/team', async function(req, res, next) {
  const pageTitle = 'Team Members';
  
  const fetchData = async () => {
    const teamMembers = await employees
      .select({maxRecords: 10, view: "Main View"})
      .eachPage(function processPage(memberRecords, getNextPage) {
        memberRecords.forEach(function(member) {
          positions.find(member.fields["Current Role"], function(error, roleData) {
            if (error) { 
              console.error("Lookup failed:", error); 
              return; 
            }
            member.fields.roleTitle = roleData.fields.Title;
            return member;
          });
        });
      });
      getNextPage();
  }
  
  fetchData()
    .then(function(teamData) {
      console.log(teamData);
      res.render('team', {title: pageTitle, teamData});
    });
});

How can I properly handle the asynchronous operations to get the actual data instead of Promise Pending?

you’re mixing callback patterns with async/await wrong. eachPage() doesn’t return data - it just runs the callback. those positions.find() calls aren’t awaited either, so they fire off without waiting. had the same issue building an employee lookup system. use .firstPage() instead of eachPage, then wrap your position lookups in proper promises. or just use the .all() method upfront.

You’re mixing promise-based code with callback-style operations. Your eachPage method doesn’t return anything, and those nested positions.find callbacks aren’t being awaited.

I hit this same mess building a staff directory that pulled from multiple Airtable bases. Callback nesting gets ugly fast.

Here’s the fix:

router.get('/team', async function(req, res, next) {
  const pageTitle = 'Team Members';
  
  try {
    // Get all employee records first
    const employeeRecords = await employees
      .select({maxRecords: 10, view: "Main View"})
      .all();
    
    // Now fetch position data for each employee
    const teamData = await Promise.all(
      employeeRecords.map(async (member) => {
        try {
          const roleData = await positions.find(member.fields["Current Role"]);
          member.fields.roleTitle = roleData.fields.Title;
          return member;
        } catch (error) {
          console.error("Lookup failed:", error);
          member.fields.roleTitle = "Unknown";
          return member;
        }
      })
    );
    
    res.render('team', {title: pageTitle, teamData});
  } catch (error) {
    console.error("Team fetch failed:", error);
    res.status(500).send("Error loading team data");
  }
});

Key changes: use .all() to get records as an array, wrap positions.find in try/catch since it can throw, and use Promise.all to handle lookups in parallel.

This stops you from mixing callback hell with async/await.

Hit this same issue building a company directory that pulled from separate Airtable bases for users and departments. The problem is eachPage() doesn’t return records - it just runs a callback for each page and returns undefined.

I fixed it by ditching eachPage() entirely. Used the all() method to grab records upfront, then handled position lookups separately.

Here’s what everyone else missed: you’ve got to promisify positions.find() since it’s callback-based. I wrapped it in a utility function that returns a promise, which made async/await actually work.

Also watch out for error handling - if any position lookup fails, your whole operation can hang. I added fallbacks so employees still show up even when their position data’s missing or the lookup times out. Made our team page way more reliable in prod.

Your eachPage() isn’t returning the records you expect, and you’re not awaiting those positions.find() calls. You’re firing off async operations without waiting for them to finish.

I hit this exact issue last year building a similar employee dashboard. The fix: collect all records first, then handle position lookups with proper promise handling.

Here’s what works: use firstPage() or all() to get your employee records into an array. Then use Promise.all() for your position lookups. You’ll need to promisify those positions.find() calls or switch to a promise-based Airtable client.

Also, you’re calling getNextPage() outside the eachPage callback - that breaks pagination. Move it inside the processPage function.

Key insight: separate data fetching from data enrichment. Get all your base data first, then enhance it with related lookups in a controlled async way.

The Problem:

You’re building a Node.js application that fetches data from two Airtable bases (employees and positions), aiming to merge employee records with their corresponding position details. Your current asynchronous function using async/await and eachPage isn’t waiting for all asynchronous operations to complete before returning, resulting in a “Promise Pending” state instead of the actual data. This is because eachPage is a callback-based method and your positions.find calls within the callback are not properly awaited. Furthermore, getNextPage() is called incorrectly outside of the eachPage callback, disrupting the pagination.

:thinking: Understanding the “Why” (The Root Cause):

The core issue lies in the improper handling of asynchronous operations within the eachPage callback. eachPage itself doesn’t return a Promise; instead, it iterates through pages of records and executes a callback function for each page. Your attempt to use async/await within this callback structure is flawed because the await keyword only works within async functions, and the positions.find calls within the callback are not directly awaited. Consequently, the fetchData function resolves before all positions.find operations are complete, leading to the “Promise Pending” result. Incorrect placement of getNextPage() further compounds this by potentially interrupting the pagination process.

:gear: Step-by-Step Guide:

Step 1: Refactor for Synchronous Data Retrieval:

Replace the eachPage method with the all() method, which directly retrieves all records in a single synchronous call. This simplifies the code and eliminates the callback-based complexity.

router.get('/team', async function(req, res, next) {
  const pageTitle = 'Team Members';

  try {
    const employeeRecords = await employees.select({ maxRecords: 10, view: "Main View" }).all();

    const teamData = await Promise.all(
      employeeRecords.map(async (member) => {
        try {
          const roleData = await positions.find(member.fields["Current Role"]);
          member.fields.roleTitle = roleData.fields.Title;
          return member;
        } catch (error) {
          console.error("Lookup failed:", error);
          //Handle the error appropriately, e.g., provide a default value:
          member.fields.roleTitle = "Role not found"; 
          return member;
        }
      })
    );

    res.render('team', { title: pageTitle, teamData });
  } catch (error) {
    console.error("Team fetch failed:", error);
    res.status(500).send("Error loading team data");
  }
});

Step 2: Handle Potential Errors:

The try...catch block handles potential errors during both the employee record fetch and the individual position lookups. The catch block provides a more graceful error handling mechanism, preventing the entire request from failing if a single position lookup encounters an issue. Consider providing a default value for roleTitle if a lookup fails, as shown above.

Step 3: Verify Airtable Configuration:

Double-check your Airtable API keys and ensure that the employees and positions variables are correctly initialized and configured to access your Airtable bases. Also, ensure that the view “Main View” exists in your employees table and that the “Current Role” field correctly maps to the corresponding field in your positions table.

:mag: Common Pitfalls & What to Check Next:

  • Airtable API Rate Limits: If you’re fetching a large number of records, Airtable’s API rate limits might be exceeded. Implement appropriate retry mechanisms and error handling for rate limit errors. Consider increasing maxRecords gradually to optimize performance.
  • Incorrect Field Names: Verify that "Current Role" and "Title" exactly match the field names in your Airtable bases, including capitalization.
  • Network Connectivity: Ensure your server has network access to the Airtable API.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

You’re not awaiting the nested async operations properly. The positions.find() callback isn’t awaited, and eachPage() doesn’t return what you think it does.

Here’s what’s broken:

  • eachPage() just iterates - it doesn’t return records
  • The positions.find() callback isn’t promisified
  • You’re mixing callbacks with async/await

What happens: fetchData() finishes before the positions lookups complete, so you get Promise Pending.

Honestly, instead of fighting Airtable’s callback hell, I’d use Latenode for this.

I’ve built similar team directories where I needed to merge data from multiple Airtable bases. With Latenode, you create a workflow that:

  1. Fetches all employee records
  2. Looks up position data for each employee
  3. Merges everything automatically
  4. Returns clean results to your Node.js app

The visual builder makes these multi-step operations way easier than wrestling with promise chains. You can add error handling, data transformation, and caching right in the workflow.

Just call one HTTP endpoint from your route instead of managing all this async mess.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.