diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go
index 739b9e056..f8e9ce0f9 100644
--- a/modules/caddyhttp/fileserver/matcher.go
+++ b/modules/caddyhttp/fileserver/matcher.go
@@ -19,6 +19,7 @@ import (
 	"net/http"
 	"os"
 	"path"
+	"strconv"
 	"strings"
 	"time"
 
@@ -60,7 +61,11 @@ type MatchFile struct {
 	// directories are treated distinctly, so to match
 	// a directory, the filepath MUST end in a forward
 	// slash `/`. To match a regular file, there must
-	// be no trailing slash. Accepts placeholders.
+	// be no trailing slash. Accepts placeholders. If
+	// the policy is "first_exist", then an error may
+	// be triggered as a fallback by configuring "="
+	// followed by a status code number,
+	// for example "=404".
 	TryFiles []string `json:"try_files,omitempty"`
 
 	// How to choose a file in TryFiles. Can be:
@@ -205,6 +210,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
 	switch m.TryPolicy {
 	case "", tryPolicyFirstExist:
 		for _, f := range m.TryFiles {
+			if err := parseErrorCode(f); err != nil {
+				caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
+				return
+			}
 			suffix, fullpath, remainder := prepareFilePath(f)
 			if info, exists := strictFileExists(fullpath); exists {
 				setPlaceholders(info, suffix, fullpath, remainder)
@@ -274,6 +283,20 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
 	return
 }
 
+// parseErrorCode checks if the input is a status
+// code number, prefixed by "=", and returns an
+// error if so.
+func parseErrorCode(input string) error {
+	if len(input) > 1 && input[0] == '=' {
+		code, err := strconv.Atoi(input[1:])
+		if err != nil || code < 100 || code > 999 {
+			return nil
+		}
+		return caddyhttp.Error(code, fmt.Errorf("%s", input[1:]))
+	}
+	return nil
+}
+
 // strictFileExists returns true if file exists
 // and matches the convention of the given file
 // path. If the path ends in a forward slash,
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index b452d48ed..a22a3f1df 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -987,6 +987,12 @@ var wordRE = regexp.MustCompile(`\w+`)
 
 const regexpPlaceholderPrefix = "http.regexp"
 
+// MatcherErrorVarKey is the key used for the variable that
+// holds an optional error emitted from a request matcher,
+// to short-circuit the handler chain, since matchers cannot
+// return errors via the RequestMatcher interface.
+const MatcherErrorVarKey = "matchers.error"
+
 // Interface guards
 var (
 	_ RequestMatcher    = (*MatchHost)(nil)
diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go
index ebd763c75..7b2871ff7 100644
--- a/modules/caddyhttp/routes.go
+++ b/modules/caddyhttp/routes.go
@@ -200,6 +200,15 @@ func wrapRoute(route Route) Middleware {
 
 			// route must match at least one of the matcher sets
 			if !route.MatcherSets.AnyMatch(req) {
+				// allow matchers the opportunity to short circuit
+				// the request and trigger the error handling chain
+				err, ok := GetVar(req.Context(), MatcherErrorVarKey).(error)
+				if ok {
+					return err
+				}
+
+				// call the next handler, and skip this one,
+				// since the matcher didn't match
 				return nextCopy.ServeHTTP(rw, req)
 			}