Function works fine in dev but 'Runtime.ImportModuleError - Error: Cannot find module '@sendgrid/mail'

I put together a fairly straight forward integration with sendgrid. Works fine when I run netlify dev locally, but when I deploy the branch to netlify to test, I get this entirely unhelpful error:

Runtime.ImportModuleError - Error: Cannot find module ‘@sendgrid/mail’

Package.json in /netlify/function looks right:

{
“name”: “functions”,
“version”: “1.0.0”,
“main”: “sendgrid.js”,
“scripts”: {
“test”: “echo "Error: no test specified" && exit 1”
},
“keywords”: ,
“author”: “”,
“license”: “ISC”,
“description”: “”,
“dependencies”: {
@sendgrid/mail”: “^8.1.3”
}
}

In any case, it works great in dev. It’s when I push the branch and netlify builds (with no errors) and I test the function that I get that error, plus this in the logs (again, not terribly helpful)

May 28, 09:09:07 PM: 49697b3a TELEMETRY	Name: netlify-observability-extension	State: Subscribed	Types: [Platform, Function]May 28, 09:09:07 PM: 49697b3a 2024-05-29T02:09:07.954Z	undefined	ERROR	Uncaught Exception 	{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module '@sendgrid/mail'\nRequire stack:\n- /var/task/sendgrid/sendgrid.js\n- /var/task/sendgrid.js\n- /var/runtime/index.mjs","stack":["Runtime.ImportModuleError: Error: Cannot find module '@sendgrid/mail'","Require stack:","- /var/task/sendgrid/sendgrid.js","- /var/task/sendgrid.js","- /var/runtime/index.mjs","    at _loadUserApp (file:///var/runtime/index.mjs:1087:17)","    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)","    at async start (file:///var/runtime/index.mjs:1282:23)","    at async file:///var/runtime/index.mjs:1288:1"]}May 28, 09:09:07 PM: 49697b3a EXTENSION	Name: netlify-observability-extension	State: Ready	Events: [INVOKE, SHUTDOWN]May 28, 09:09:07 PM: 49697b3a INIT_REPORT Init Duration: 236.76 ms	Phase: init	Status: error	Error Type: Runtime.ExitErrorMay 28, 09:09:07 PM: 49697b3a TELEMETRY	Name: netlify-observability-extension	State: Already subscribed	Types: [Platform, Function]May 28, 09:09:07 PM: 49697b3a 2024-05-29T02:09:08.268Z	undefined	ERROR	Uncaught Exception 	{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module '@sendgrid/mail'\nRequire stack:\n- /var/task/sendgrid/sendgrid.js\n- /var/task/sendgrid.js\n- /var/runtime/index.mjs","stack":["Runtime.ImportModuleError: Error: Cannot find module '@sendgrid/mail'","Require stack:","- /var/task/sendgrid/sendgrid.js","- /var/task/sendgrid.js","- /var/runtime/index.mjs","    at _loadUserApp (file:///var/runtime/index.mjs:1087:17)","    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)","    at async start (file:///var/runtime/index.mjs:1282:23)","    at async file:///var/runtime/index.mjs:1288:1"]}May 28, 09:09:07 PM: 49697b3a EXTENSION	Name: netlify-observability-extension	State: Ready	Events: [SHUTDOWN, INVOKE]May 28, 09:09:07 PM: 49697b3a INIT_REPORT Init Duration: 304.01 ms	Phase: invoke	Status: error	Error Type: Runtime.ExitErrorMay 28, 09:09:07 PM: 49697b3a Unknown application error occurredMay 28, 09:09:07 PM: 49697b3a Runtime.ImportModuleErrorMay 28, 09:09:08 PM: 49697b3a Duration: 476.50 ms	Memory Usage: 24 MB	

Why might my function not be able to find the sendgrid package when it does in dev, and it’s in package.json (both in the root of the Astro SSG site as well as in ‘netlify/function?’

const sendgridMail = require("@sendgrid/mail");
const { SENDGRID_API_KEY } = process.env;

const handler = async function (event) {
  // get data from body
  const stuff = JSON.parse(event.body);
  sendgridMail.setApiKey(SENDGRID_API_KEY);

  console.log(stuff);
  // set API key
  console.log(stuff.firstName);
  // setup data for email
  // NOTE: THIS IS NOT SECURE. YOU NEED TO SANITIZE THE INPUTS
  let htmlContent = '<div>';
  for (const [key, value] of Object.entries(stuff)) {
    htmlContent += `<p><strong>${key}:</strong> ${value}</p>`;
  }
  htmlContent += '</div>';

  const html = `<div>${htmlContent}</div>`;

  const data = {
    to: "REDACTED", // Change to your recipient (your email in this case)
    from: "REDACTED", // Change to your verified sender
    subject: `New message from ${stuff.firstName} ${stuff.lastName}`,
    html: html,
  };

  try {
    console.log(data);
    await sendgridMail.send(data);
    return {
      statusCode: 200,
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        msg: "Message sent successfully",
      }),
    };
  } catch (err) {
    console.log(err.code)
    return {
      statusCode: err.code,
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ msg: err.message }),
    };
  }
};

export { handler };

Any ideas?

Could you try to add:

[functions]
  node_bundler = "esbuild"

OR

[functions]
  external_node_modules = ["@sendgrid/mail"]

in your netlify.toml?

Adding

[functions]
   external_node_modules = ["@sendgrid/mail"]

Seemed to work.

Can you explain why?
I don’t have this on another machine using the same repo, which runs netlify build without any issue.
cf: Netlify build works on one machine, build fails on the other

If adding that works it means @sendgrid/mail is being imported in your application in a way that it is not getting bundled in a regular manner, so the above basically forces that dependency to exist in your bundle.

When we detect the requirement for such a thing, we usually log a warning saying that might be required (for example wirth express).