✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

EcosystemIntermediate6 min read

"Cannot read properties of undefined (reading 'attributes')" in Strapi

September 9, 2026
"Cannot read properties of undefined (reading 'attributes')" in Strapi

Strapi refuses to start:

debug: ⛔️ Server wasn't able to start properly.
error: Cannot read properties of undefined (reading 'attributes')
TypeError: Cannot read properties of undefined (reading 'attributes')
    at Object.getNonVisibleAttributes (.../@strapi/utils/lib/content-types.js)
    at getNestedFields (.../@strapi/admin/server/services/content-type.js)
    ...

The most upvoted advice on this error, historically, has been "recreate the project". You do not need to.

What it means

Strapi is walking your content types to build the admin panel's field configuration. For each attribute it needs to resolve the target schema, either a component or the content type on the other end of a relation, and read its attributes.

One of those lookups returned undefined, and the code then tried to read .attributes off nothing.

Translation: a content type references something that does not exist. Almost always a component; occasionally a relation, a dynamic zone entry, or a custom field whose plugin is not installed.

Find the broken reference

Every component reference vs. every component file

This one-liner catches the large majority of cases:

grep -rho '"component": "[^"]*"' src/api src/components \
  | sed 's/.*: "//; s/"//' | sort -u \
  | while read c; do
      f="src/components/${c/./\/}.json"
      [ -f "$f" ] || echo "MISSING: $c  (expected $f)"
    done

A component named shared.seo must exist at src/components/shared/seo.json. Anything printed is a reference with no file behind it.

Dynamic zones

Dynamic zones list their allowed components in a components array, which the grep above does not cover:

"blocks": {
  "type": "dynamiczone",
  "components": ["layout.hero", "layout.cta", "shared.quote"]
}

Check those too:

grep -rA10 '"type": "dynamiczone"' src/api src/components \
  | grep -o '"[a-z0-9-]*\.[a-z0-9-]*"' | tr -d '"' | sort -u \
  | while read c; do
      f="src/components/${c/./\/}.json"
      [ -f "$f" ] || echo "MISSING (dynamiczone): $c"
    done

Relations pointing at deleted content types

grep -rho '"target": "[^"]*"' src/api src/components \
  | sed 's/.*: "//; s/"//' | sort -u

Each api::foo.foo target needs src/api/foo/content-types/foo/schema.json to exist. Targets starting plugin:: refer to plugins, so plugin::users-permissions.user is fine as long as that plugin is installed.

Nested components

A component can reference another component. If layout.hero includes shared.button and shared/button.json was deleted, the failure surfaces when the parent is loaded, so the error will point you at hero rather than at the actual missing file. The first grep catches this because it searches src/components as well as src/api.

Custom fields from uninstalled plugins

grep -rn '"customField"' src/api src/components

A "customField": "plugin::color-picker.color" on a content type whose plugin is no longer in package.json produces the same class of failure. Either reinstall the plugin or remove the attribute.

The migration case

This error is common right after a v3 → v4 or v4 → v5 migration, and the reason is structural: components moved location and naming conventions changed between major versions. Codemods handle most of it and reliably miss edge cases. As the original forum thread found, the root cause there was components that were never moved to their v4 location.

After running any migration tool:

  1. Run the checks above before doing anything else.
  2. Diff src/components/ against the old project and confirm every file arrived.
  3. Open a few schema.json files by hand and confirm the shape looks like the current version's.

For a v4 → v5 move specifically, use the upgrade tool and read the migration guide, because several attribute-level changes are documented there and are easy to miss.

Fixing it

Once you know which reference is broken, you have two options.

Restore the missing file

If it was deleted by accident, get it back:

git log --diff-filter=D --name-only -- src/components/
git checkout <commit>^ -- src/components/shared/seo.json

This is the better outcome, because the database table for that component still exists and the content in it is preserved.

Remove the reference

If the component is genuinely gone, edit the content type's schema.json and delete the attribute:

{
  "kind": "collectionType",
  "collectionName": "articles",
  "info": { "singularName": "article", "pluralName": "articles", "displayName": "Article" },
  "options": { "draftAndPublish": true },
  "attributes": {
    "title": { "type": "string" },
    "body": { "type": "richtext" }
    // "seo": { "type": "component", "component": "shared.seo" }   ← removed
  }
}

Understand what this costs. On the next startup, Strapi's schema sync compares your content types with the database and drops tables, columns, and indexes it previously managed that no longer match. Removing the attribute means the component's data is deleted, without a prompt, in development and production alike.

If the data matters:

  • Take a database backup first.
  • Or set forceMigration: false in config/database settings to skip drop operations, retrieve what you need, then decide. Note the caveat in the docs: the new schema is still recorded as the reference, so an object whose deletion was skipped stops being tracked and will not be dropped later if you set the flag back.

Getting a name out of the error

The stack trace does not tell you which content type failed. A temporary log in register will:

export default {
  register({ strapi }) {
    for (const [uid, ct] of Object.entries(strapi.contentTypes)) {
      for (const [name, attr] of Object.entries<any>(ct.attributes ?? {})) {
        if (attr.type === 'component' && !strapi.components[attr.component]) {
          strapi.log.error(`Broken component reference: ${uid}.${name} → ${attr.component}`);
        }
        if (attr.type === 'dynamiczone') {
          for (const c of attr.components ?? []) {
            if (!strapi.components[c]) {
              strapi.log.error(`Broken dynamiczone reference: ${uid}.${name} → ${c}`);
            }
          }
        }
      }
    }
  },
  bootstrap() {},
};

This runs early enough to print before the admin service crashes, so you get names instead of frames. Remove it once you have the answer.

Recovering from a boot loop

You cannot use the admin panel when Strapi will not start, so:

  1. git status and git diff: the change is usually right there.
  2. git stash to test whether uncommitted work caused it.
  3. Move suspect files out of the tree rather than deleting them: mv src/components/shared/seo.json /tmp/.
  4. Clear stale build output: rm -rf dist .cache build .strapi && npm run build.

Preventing it

  • Never delete a component file by hand. Use the Content-Type Builder, which removes references at the same time.
  • Commit src/components/ and src/api/. These are code, and a schema diff in review catches broken references before they reach anyone else.
  • Boot Strapi in CI against a throwaway database. Every error in this post fails at startup, so a job that starts the server catches all of them at pull-request time.
  • After any migration, run the grep checks above before you start debugging anything else.

ERR_INVALID_ARG_TYPE: The "path" argument must be of type string is the sibling of this error: same underlying category, different failure point in the loader.

Further reading

Theodore Kelechukwu OnyejiakuDevRel and Community | Software Developer | Technical Writer

Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.

Seeding Data in Strapi: Why Migrations are the Wrong Tool
EcosystemIntermediate·7 min read

Seeding Data in Strapi: Why Migrations are the Wrong Tool

Migrations run before Strapi's schema sync, so seeding a brand-new content type fails on the missing table. Use bootstrap instead, idempotently.

·September 9, 2026
Strapi and TypeScript: compiling when there are type errors
EcosystemAdvanced·6 min read

Strapi and TypeScript: compiling when there are type errors

A single type error stops your Strapi dev server. The tsconfig settings that let it build anyway, and when each one is actually appropriate.

·September 9, 2026
How to debug a 500 Internal Server Error in Strapi
EcosystemBeginner·7 min read

How to debug a 500 Internal Server Error in Strapi

A 500 means Strapi threw an exception it did not expect. The real message is in your server logs, not the browser. A systematic way to find it.

·September 9, 2026