Repository: tailwindlabs/tailwindcss

Problem statement:
Is it intentional that `theme()` doesn't throw when property doesn't exist?
This seems like maybe a bug, or at least an undocumented design choice. I was expecting `theme` to provide safety rails against typos or unexpected theme breakage by throwing an error if you try and get something that doesn't exist from the config. It doesn't tho, which is all very css but seems like an unnecessary footgun. Wondering if it's a bug to be fixed or some thoughts on why it fails silently

thanks!


Hints:
Could definitely make it throw when someone asks for a key that doesn't exist 👍 Would accept a PR for it for sure.

Patch:
diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js
--- a/src/lib/evaluateTailwindFunctions.js
+++ b/src/lib/evaluateTailwindFunctions.js
@@ -1,6 +1,34 @@
 import _ from 'lodash'
 import functions from 'postcss-functions'
 
+function findClosestExistingPath(theme, path) {
+  const parts = _.toPath(path)
+  do {
+    parts.pop()
+
+    if (_.hasIn(theme, parts)) break
+  } while (parts.length)
+
+  return parts
+}
+
+function buildError(root, theme, path) {
+  const closestPath = findClosestExistingPath(theme, path).join('.') || 'theme'
+  const closestValue = _.get(theme, closestPath, theme)
+
+  let message = `"${path}" does not exist in your tailwind theme.\n`
+
+  if (typeof closestValue === 'object') {
+    message += `Valid keys for "${closestPath}" are: ${Object.keys(closestValue)
+      .map(k => `"${k}"`)
+      .join(', ')}`
+  } else {
+    message += `"${closestPath}" exists but is not an object.`
+  }
+
+  return root.error(message)
+}
+
 const themeTransforms = {
   fontSize(value) {
     return Array.isArray(value) ? value[0] : value
@@ -12,16 +40,22 @@ function defaultTransform(value) {
 }
 
 export default function(config) {
-  return functions({
-    functions: {
-      theme: (path, ...defaultValue) => {
-        const trimmedPath = _.trim(path, `'"`)
-        return _.thru(_.get(config.theme, trimmedPath, defaultValue), value => {
-          const [themeSection] = trimmedPath.split('.')
-
-          return _.get(themeTransforms, themeSection, defaultTransform)(value)
-        })
+  return root =>
+    functions({
+      functions: {
+        theme: (path, ...defaultValue) => {
+          const trimmedPath = _.trim(path, `'"`)
+
+          if (!defaultValue.length && !_.hasIn(config.theme, trimmedPath)) {
+            throw buildError(root, config.theme, trimmedPath)
+          }
+
+          return _.thru(_.get(config.theme, trimmedPath, defaultValue), value => {
+            const [themeSection] = trimmedPath.split('.')
+
+            return _.get(themeTransforms, themeSection, defaultTransform)(value)
+          })
+        },
       },
-    },
-  })
+    })(root)
 }


Test patch:
diff --git a/__tests__/themeFunction.test.js b/__tests__/themeFunction.test.js
--- a/__tests__/themeFunction.test.js
+++ b/__tests__/themeFunction.test.js
@@ -110,6 +110,61 @@ test('an unquoted list is valid as a default value', () => {
   })
 })
 
+test('A missing root theme value throws', () => {
+  const input = `
+    .heading { color: theme('colours.gray.100'); }
+  `
+
+  return expect(
+    run(input, {
+      theme: {
+        colors: {
+          yellow: '#f7cc50',
+        },
+      },
+    })
+  ).rejects.toThrowError(
+    '"colours.gray.100" does not exist in your tailwind theme.\nValid keys for "theme" are: "colors"'
+  )
+})
+
+test('A missing nested theme property throws', () => {
+  const input = `
+    .heading { color: theme('colors.red'); }
+  `
+
+  return expect(
+    run(input, {
+      theme: {
+        colors: {
+          blue: 'blue',
+          yellow: '#f7cc50',
+        },
+      },
+    })
+  ).rejects.toThrowError(
+    '"colors.red" does not exist in your tailwind theme.\nValid keys for "colors" are: "blue", "yellow"'
+  )
+})
+
+test('A path through a non-object throws', () => {
+  const input = `
+    .heading { color: theme('colors.yellow.100'); }
+  `
+
+  return expect(
+    run(input, {
+      theme: {
+        colors: {
+          yellow: '#f7cc50',
+        },
+      },
+    })
+  ).rejects.toThrowError(
+    '"colors.yellow.100" does not exist in your tailwind theme.\n"colors.yellow" exists but is not an object'
+  )
+})
+
 test('array values are joined by default', () => {
   const input = `
     .heading { font-family: theme('fontFamily.sans'); }