Repository: tailwindlabs/tailwindcss

Problem statement:
Angular 19 cannot automatically recover using @apply with Tailwind CSS 4
Tailwind CSS: 4.0.14
Angular: 19.2.4
Node: 23.6.0
Chrome: 134.0.6998.89 (Official Build) (arm64)
MacOS: Sequoia 15.3.2 (24D81)

**Reproduction URL**

[https://github.com/micobarac/angular19](https://github.com/micobarac/angular19)

**Describe your issue**

Reproducing the issue:

- run `yarn install && yarn start`
- open `src/tailwind.css`
- change `bg-white` class within `@apply` directive to `bg-unknown` at line 4
- observe the error: 

```
✘ [ERROR] Cannot apply unknown utility class: bg-unknown [plugin angular-css]

    angular:styles/global:styles:1:8:
      1 │ @import 'src/styles.css';
```

- change `bg-unknown` class within `@apply` directive back to `bg-white` at line 4
- observe the error (**Angular should have recovered by now**): 

```
✘ [ERROR] Cannot apply unknown utility class: bg-unknown [plugin angular-css]

    angular:styles/global:styles:1:8:
      1 │ @import 'src/styles.css';
```

I can't tell if this problem resides inside `Angular 19` or `Tailwind CSS 4`.

I opened issue here:

[https://github.com/angular/angular-cli/issues/29789](https://github.com/angular/angular-cli/issues/29789)

[https://github.com/angular/angular-cli/issues/29789#issuecomment-2737483148](https://github.com/angular/angular-cli/issues/29789#issuecomment-2737483148)

_In Tailwind CSS version 4, syntax errors trigger an exception instead of a PostCSS error. This causes PostCSS to crash, preventing it from tracking the files Tailwind processes. As a result, these files aren't added to the file watcher / dependency tree, leading to issues where cached compilation results aren't invalidated when a file changes, since it isn't recognized as a transitive dependency._

I opened discussion here, too:

[https://github.com/tailwindlabs/tailwindcss/discussions/17077](https://github.com/tailwindlabs/tailwindcss/discussions/17077)


Hints:
Same problem with Nuxt (vue js)
What's worse, Angular team does not plan to fix this, stating:

_Closing as there isn’t much we can do here.
Tailwind is throwing an exception that causes the underlying PostCSS instance to crash and not return which files to watch._
I am guessing it is crashing here ... https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/apply.ts#L166

I am guessing If some tests will be added, problem will be fixed soon ...
having this issue as well; it's inconsistent—the error always pops up on initial build, but if new builds are generated during `ng serve`, it will work just fine
I am experiencing the same issue with the latest version of Angular. CSS changes are not reflecting without restarting ng serve when using Tailwind CSS V4. Here is my Angular CLI version
Angular CLI: 19.2.8
Node: 22.14.0
Package Manager: npm 11.1.0
Angular: 19.2.7

Application bundle generation failed. [0.479 seconds]

✘ [ERROR] Cannot apply unknown utility class: md:w-auto [plugin angular-css]


Experiencing this problem a well. We have a lot of `@apply` statements in a big project, and it would require a lot of refactoring to change this. Any known workaround ?
Same problem, so I switched back to TW 3.x.

Patch:
diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts
--- a/packages/@tailwindcss-postcss/src/index.ts
+++ b/packages/@tailwindcss-postcss/src/index.ts
@@ -103,6 +103,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
           let context = getContextFromCache(inputFile, opts)
           let inputBasePath = path.dirname(path.resolve(inputFile))
 
+          // Whether this is the first build or not, if it is, then we can
+          // optimize the build by not creating the compiler until we need it.
+          let isInitialBuild = context.compiler === null
+
           async function createCompiler() {
             DEBUG && I.start('Setup compiler')
             if (context.fullRebuildPaths.length > 0 && !isInitialBuild) {
@@ -119,9 +123,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
             let compiler = await compileAst(ast, {
               base: inputBasePath,
               shouldRewriteUrls: true,
-              onDependency: (path) => {
-                context.fullRebuildPaths.push(path)
-              },
+              onDependency: (path) => context.fullRebuildPaths.push(path),
               // In CSS Module files, we have to disable the `@property` polyfill since these will
               // emit global `*` rules which are considered to be non-pure and will cause builds
               // to fail.
@@ -133,190 +135,207 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
             return compiler
           }
 
-          // Whether this is the first build or not, if it is, then we can
-          // optimize the build by not creating the compiler until we need it.
-          let isInitialBuild = context.compiler === null
+          try {
+            // Setup the compiler if it doesn't exist yet. This way we can
+            // guarantee a `build()` function is available.
+            context.compiler ??= createCompiler()
 
-          // Setup the compiler if it doesn't exist yet. This way we can
-          // guarantee a `build()` function is available.
-          context.compiler ??= createCompiler()
+            if ((await context.compiler).features === Features.None) {
+              return
+            }
 
-          if ((await context.compiler).features === Features.None) {
-            return
-          }
+            let rebuildStrategy: 'full' | 'incremental' = 'incremental'
 
-          let rebuildStrategy: 'full' | 'incremental' = 'incremental'
+            // Track file modification times to CSS files
+            DEBUG && I.start('Register full rebuild paths')
+            {
+              for (let file of context.fullRebuildPaths) {
+                result.messages.push({
+                  type: 'dependency',
+                  plugin: '@tailwindcss/postcss',
+                  file: path.resolve(file),
+                  parent: result.opts.from,
+                })
+              }
 
-          // Track file modification times to CSS files
-          DEBUG && I.start('Register full rebuild paths')
-          {
-            for (let file of context.fullRebuildPaths) {
-              result.messages.push({
-                type: 'dependency',
-                plugin: '@tailwindcss/postcss',
-                file: path.resolve(file),
-                parent: result.opts.from,
+              let files = result.messages.flatMap((message) => {
+                if (message.type !== 'dependency') return []
+                return message.file
               })
-            }
-
-            let files = result.messages.flatMap((message) => {
-              if (message.type !== 'dependency') return []
-              return message.file
-            })
-            files.push(inputFile)
-
-            for (let file of files) {
-              let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null
-              if (changedTime === null) {
-                if (file === inputFile) {
-                  rebuildStrategy = 'full'
+              files.push(inputFile)
+
+              for (let file of files) {
+                let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null
+                if (changedTime === null) {
+                  if (file === inputFile) {
+                    rebuildStrategy = 'full'
+                  }
+                  continue
                 }
-                continue
-              }
 
-              let prevTime = context.mtimes.get(file)
-              if (prevTime === changedTime) continue
+                let prevTime = context.mtimes.get(file)
+                if (prevTime === changedTime) continue
 
-              rebuildStrategy = 'full'
-              context.mtimes.set(file, changedTime)
+                rebuildStrategy = 'full'
+                context.mtimes.set(file, changedTime)
+              }
+            }
+            DEBUG && I.end('Register full rebuild paths')
+
+            if (
+              rebuildStrategy === 'full' &&
+              // We can re-use the compiler if it was created during the
+              // initial build. If it wasn't, we need to create a new one.
+              !isInitialBuild
+            ) {
+              context.compiler = createCompiler()
             }
-          }
-          DEBUG && I.end('Register full rebuild paths')
-
-          if (
-            rebuildStrategy === 'full' &&
-            // We can re-use the compiler if it was created during the
-            // initial build. If it wasn't, we need to create a new one.
-            !isInitialBuild
-          ) {
-            context.compiler = createCompiler()
-          }
 
-          let compiler = await context.compiler
+            let compiler = await context.compiler
 
-          if (context.scanner === null || rebuildStrategy === 'full') {
-            DEBUG && I.start('Setup scanner')
-            let sources = (() => {
-              // Disable auto source detection
-              if (compiler.root === 'none') {
-                return []
-              }
-
-              // No root specified, use the base directory
-              if (compiler.root === null) {
-                return [{ base, pattern: '**/*', negated: false }]
-              }
+            if (context.scanner === null || rebuildStrategy === 'full') {
+              DEBUG && I.start('Setup scanner')
+              let sources = (() => {
+                // Disable auto source detection
+                if (compiler.root === 'none') {
+                  return []
+                }
 
-              // Use the specified root
-              return [{ ...compiler.root, negated: false }]
-            })().concat(compiler.sources)
+                // No root specified, use the base directory
+                if (compiler.root === null) {
+                  return [{ base, pattern: '**/*', negated: false }]
+                }
 
-            // Look for candidates used to generate the CSS
-            context.scanner = new Scanner({ sources })
-            DEBUG && I.end('Setup scanner')
-          }
+                // Use the specified root
+                return [{ ...compiler.root, negated: false }]
+              })().concat(compiler.sources)
 
-          DEBUG && I.start('Scan for candidates')
-          let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : []
-          DEBUG && I.end('Scan for candidates')
-
-          if (compiler.features & Features.Utilities) {
-            DEBUG && I.start('Register dependency messages')
-            // Add all found files as direct dependencies
-            // Note: With Turbopack, the input file might not be a resolved path
-            let resolvedInputFile = path.resolve(base, inputFile)
-            for (let file of context.scanner.files) {
-              let absolutePath = path.resolve(file)
-              // The CSS file cannot be a dependency of itself
-              if (absolutePath === resolvedInputFile) {
-                continue
-              }
-              result.messages.push({
-                type: 'dependency',
-                plugin: '@tailwindcss/postcss',
-                file: absolutePath,
-                parent: result.opts.from,
-              })
+              // Look for candidates used to generate the CSS
+              context.scanner = new Scanner({ sources })
+              DEBUG && I.end('Setup scanner')
             }
 
-            // Register dependencies so changes in `base` cause a rebuild while
-            // giving tools like Vite or Parcel a glob that can be used to limit
-            // the files that cause a rebuild to only those that match it.
-            for (let { base: globBase, pattern } of context.scanner.globs) {
-              // Avoid adding a dependency on the base directory itself, since it
-              // causes Next.js to start an endless recursion if the `distDir` is
-              // configured to anything other than the default `.next` dir.
-              if (pattern === '*' && base === globBase) {
-                continue
-              }
-
-              if (pattern === '') {
+            DEBUG && I.start('Scan for candidates')
+            let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : []
+            DEBUG && I.end('Scan for candidates')
+
+            if (compiler.features & Features.Utilities) {
+              DEBUG && I.start('Register dependency messages')
+              // Add all found files as direct dependencies
+              // Note: With Turbopack, the input file might not be a resolved path
+              let resolvedInputFile = path.resolve(base, inputFile)
+              for (let file of context.scanner.files) {
+                let absolutePath = path.resolve(file)
+                // The CSS file cannot be a dependency of itself
+                if (absolutePath === resolvedInputFile) {
+                  continue
+                }
                 result.messages.push({
                   type: 'dependency',
                   plugin: '@tailwindcss/postcss',
-                  file: path.resolve(globBase),
-                  parent: result.opts.from,
-                })
-              } else {
-                result.messages.push({
-                  type: 'dir-dependency',
-                  plugin: '@tailwindcss/postcss',
-                  dir: path.resolve(globBase),
-                  glob: pattern,
+                  file: absolutePath,
                   parent: result.opts.from,
                 })
               }
+
+              // Register dependencies so changes in `base` cause a rebuild while
+              // giving tools like Vite or Parcel a glob that can be used to limit
+              // the files that cause a rebuild to only those that match it.
+              for (let { base: globBase, pattern } of context.scanner.globs) {
+                // Avoid adding a dependency on the base directory itself, since it
+                // causes Next.js to start an endless recursion if the `distDir` is
+                // configured to anything other than the default `.next` dir.
+                if (pattern === '*' && base === globBase) {
+                  continue
+                }
+
+                if (pattern === '') {
+                  result.messages.push({
+                    type: 'dependency',
+                    plugin: '@tailwindcss/postcss',
+                    file: path.resolve(globBase),
+                    parent: result.opts.from,
+                  })
+                } else {
+                  result.messages.push({
+                    type: 'dir-dependency',
+                    plugin: '@tailwindcss/postcss',
+                    dir: path.resolve(globBase),
+                    glob: pattern,
+                    parent: result.opts.from,
+                  })
+                }
+              }
+              DEBUG && I.end('Register dependency messages')
             }
-            DEBUG && I.end('Register dependency messages')
-          }
 
-          DEBUG && I.start('Build utilities')
-          let tailwindCssAst = compiler.build(candidates)
-          DEBUG && I.end('Build utilities')
+            DEBUG && I.start('Build utilities')
+            let tailwindCssAst = compiler.build(candidates)
+            DEBUG && I.end('Build utilities')
 
-          if (context.tailwindCssAst !== tailwindCssAst) {
-            if (optimize) {
-              DEBUG && I.start('Optimization')
+            if (context.tailwindCssAst !== tailwindCssAst) {
+              if (optimize) {
+                DEBUG && I.start('Optimization')
 
-              DEBUG && I.start('AST -> CSS')
-              let css = toCss(tailwindCssAst)
-              DEBUG && I.end('AST -> CSS')
+                DEBUG && I.start('AST -> CSS')
+                let css = toCss(tailwindCssAst)
+                DEBUG && I.end('AST -> CSS')
 
-              DEBUG && I.start('Lightning CSS')
-              let ast = optimizeCss(css, {
-                minify: typeof optimize === 'object' ? optimize.minify : true,
-              })
-              DEBUG && I.end('Lightning CSS')
-
-              DEBUG && I.start('CSS -> PostCSS AST')
-              context.optimizedPostCssAst = postcss.parse(ast, result.opts)
-              DEBUG && I.end('CSS -> PostCSS AST')
-
-              DEBUG && I.end('Optimization')
-            } else {
-              // Convert our AST to a PostCSS AST
-              DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST')
-              context.cachedPostCssAst = cssAstToPostCssAst(tailwindCssAst, root.source)
-              DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST')
+                DEBUG && I.start('Lightning CSS')
+                let ast = optimizeCss(css, {
+                  minify: typeof optimize === 'object' ? optimize.minify : true,
+                })
+                DEBUG && I.end('Lightning CSS')
+
+                DEBUG && I.start('CSS -> PostCSS AST')
+                context.optimizedPostCssAst = postcss.parse(ast, result.opts)
+                DEBUG && I.end('CSS -> PostCSS AST')
+
+                DEBUG && I.end('Optimization')
+              } else {
+                // Convert our AST to a PostCSS AST
+                DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST')
+                context.cachedPostCssAst = cssAstToPostCssAst(tailwindCssAst, root.source)
+                DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST')
+              }
             }
-          }
 
-          context.tailwindCssAst = tailwindCssAst
+            context.tailwindCssAst = tailwindCssAst
+
+            DEBUG && I.start('Update PostCSS AST')
+            root.removeAll()
+            root.append(
+              optimize
+                ? context.optimizedPostCssAst.clone().nodes
+                : context.cachedPostCssAst.clone().nodes,
+            )
+
+            // Trick PostCSS into thinking the indent is 2 spaces, so it uses that
+            // as the default instead of 4.
+            root.raws.indent = '  '
+            DEBUG && I.end('Update PostCSS AST')
 
-          DEBUG && I.start('Update PostCSS AST')
-          root.removeAll()
-          root.append(
-            optimize
-              ? context.optimizedPostCssAst.clone().nodes
-              : context.cachedPostCssAst.clone().nodes,
-          )
+            DEBUG && I.end(`[@tailwindcss/postcss] ${relative(base, inputFile)}`)
+          } catch (error) {
+            // An error requires a full rebuild to fix
+            context.compiler = null
 
-          // Trick PostCSS into thinking the indent is 2 spaces, so it uses that
-          // as the default instead of 4.
-          root.raws.indent = '  '
-          DEBUG && I.end('Update PostCSS AST')
+            // Ensure all dependencies we have collected thus far are included so that the rebuild
+            // is correctly triggered
+            for (let file of context.fullRebuildPaths) {
+              result.messages.push({
+                type: 'dependency',
+                plugin: '@tailwindcss/postcss',
+                file: path.resolve(file),
+                parent: result.opts.from,
+              })
+            }
 
-          DEBUG && I.end(`[@tailwindcss/postcss] ${relative(base, inputFile)}`)
+            // We found that throwing the error will cause PostCSS to no longer watch for changes
+            // in some situations so we instead log the error and continue with an empty stylesheet.
+            console.error(error)
+            root.removeAll()
+          }
         },
       },
     ],


Test patch:
diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts
--- a/integrations/postcss/index.test.ts
+++ b/integrations/postcss/index.test.ts
@@ -635,3 +635,91 @@ test(
     await fs.expectFileToContain('project-a/dist/out.css', [candidate`content-['c/src/index.js']`])
   },
 )
+
+test(
+  'rebuild error recovery',
+  {
+    fs: {
+      'package.json': json`
+        {
+          "devDependencies": {
+            "postcss": "^8",
+            "postcss-cli": "^10",
+            "tailwindcss": "workspace:^",
+            "@tailwindcss/postcss": "workspace:^"
+          }
+        }
+      `,
+      'postcss.config.js': js`
+        module.exports = {
+          plugins: {
+            '@tailwindcss/postcss': {},
+          },
+        }
+      `,
+      'src/index.html': html`
+        <div class="underline"></div>
+      `,
+      'src/index.css': css` @import './tailwind.css'; `,
+      'src/tailwind.css': css`
+        @reference 'tailwindcss/does-not-exist';
+        @import 'tailwindcss/utilities';
+      `,
+    },
+  },
+  async ({ fs, expect, spawn }) => {
+    let process = await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+
+    await process.onStderr((message) =>
+      message.includes('Package path ./does-not-exist is not exported from package'),
+    )
+
+    expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
+      "
+      --- dist/out.css ---
+      <EMPTY>
+      "
+    `)
+
+    await process.onStderr((message) => message.includes('Waiting for file changes...'))
+
+    // Fix the CSS file
+    await fs.write(
+      'src/tailwind.css',
+      css`
+        @reference 'tailwindcss/theme';
+        @import 'tailwindcss/utilities';
+      `,
+    )
+    await process.onStderr((message) => message.includes('Finished src/index.css'))
+
+    expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
+      "
+      --- dist/out.css ---
+      .underline {
+        text-decoration-line: underline;
+      }
+      "
+    `)
+
+    // Now break the CSS file again
+    await fs.write(
+      'src/tailwind.css',
+      css`
+        @reference 'tailwindcss/does-not-exist';
+        @import 'tailwindcss/utilities';
+      `,
+    )
+    await process.onStderr((message) =>
+      message.includes('Package path ./does-not-exist is not exported from package'),
+    )
+    await process.onStderr((message) => message.includes('Finished src/index.css'))
+
+    expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
+      "
+      --- dist/out.css ---
+      <EMPTY>
+      "
+    `)
+  },
+)
diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts
--- a/packages/tailwindcss/src/compat/config.test.ts
+++ b/packages/tailwindcss/src/compat/config.test.ts
@@ -1217,7 +1217,7 @@ test('utilities used in @apply must be prefixed', async () => {
   `)
 
   // Non-prefixed utilities cause an error
-  expect(() =>
+  await expect(
     compile(
       css`
         @config "./config.js";
@@ -1405,7 +1405,7 @@ test('blocklisted candidates are not generated', async () => {
 })
 
 test('blocklisted candidates cannot be used with `@apply`', async () => {
-  await expect(() =>
+  await expect(
     compile(
       css`
         @theme reference {
diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts
--- a/packages/tailwindcss/src/index.test.ts
+++ b/packages/tailwindcss/src/index.test.ts
@@ -454,9 +454,9 @@ describe('@apply', () => {
     `)
   })
 
-  it('should error when using @apply with a utility that does not exist', () => {
-    return expect(
-      compileCss(css`
+  it('should error when using @apply with a utility that does not exist', async () => {
+    await expect(
+      compile(css`
         @tailwind utilities;
 
         .foo {
@@ -468,9 +468,9 @@ describe('@apply', () => {
     )
   })
 
-  it('should error when using @apply with a variant that does not exist', () => {
-    return expect(
-      compileCss(css`
+  it('should error when using @apply with a variant that does not exist', async () => {
+    await expect(
+      compile(css`
         @tailwind utilities;
 
         .foo {
diff --git a/packages/tailwindcss/src/prefix.test.ts b/packages/tailwindcss/src/prefix.test.ts
--- a/packages/tailwindcss/src/prefix.test.ts
+++ b/packages/tailwindcss/src/prefix.test.ts
@@ -80,7 +80,7 @@ test('utilities used in @apply must be prefixed', async () => {
   `)
 
   // Non-prefixed utilities cause an error
-  expect(() =>
+  await expect(
     compile(css`
       @theme reference prefix(tw);