diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index b758c39db..7ad4a2016 100644
--- a/caddyconfig/httpcaddyfile/builtins.go
+++ b/caddyconfig/httpcaddyfile/builtins.go
@@ -35,7 +35,8 @@ func init() {
 	RegisterHandlerDirective("redir", parseRedir)
 	RegisterHandlerDirective("respond", parseRespond)
 	RegisterHandlerDirective("route", parseRoute)
-	RegisterHandlerDirective("handle", parseHandle)
+	RegisterHandlerDirective("handle", parseSegmentAsSubroute)
+	RegisterDirective("handle_errors", parseHandleErrors)
 }
 
 // parseBind parses the bind directive. Syntax:
@@ -235,7 +236,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
 					return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
 				}
 				mgr.Challenges.DNSRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, h.warnings)
-			
+
 			case "ca_root":
 				arg := h.RemainingArgs()
 				if len(arg) != 1 {
@@ -387,36 +388,21 @@ func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
 	return sr, nil
 }
 
-// parseHandle parses the route directive.
 func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
-	var allResults []ConfigValue
+	return parseSegmentAsSubroute(h)
+}
 
-	for h.Next() {
-		for nesting := h.Nesting(); h.NextBlock(nesting); {
-			dir := h.Val()
-
-			dirFunc, ok := registeredDirectives[dir]
-			if !ok {
-				return nil, h.Errf("unrecognized directive: %s", dir)
-			}
-
-			subHelper := h
-			subHelper.Dispenser = h.NewFromNextSegment()
-
-			results, err := dirFunc(subHelper)
-			if err != nil {
-				return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
-			}
-			for _, result := range results {
-				result.directive = dir
-				allResults = append(allResults, result)
-			}
-		}
-
-		return buildSubroute(allResults, h.groupCounter)
+func parseHandleErrors(h Helper) ([]ConfigValue, error) {
+	subroute, err := parseSegmentAsSubroute(h)
+	if err != nil {
+		return nil, err
 	}
-
-	return nil, nil
+	return []ConfigValue{
+		{
+			Class: "error_route",
+			Value: subroute,
+		},
+	}, nil
 }
 
 var tagCounter = 0
diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go
index 035dcbece..3c03d309c 100644
--- a/caddyconfig/httpcaddyfile/directives.go
+++ b/caddyconfig/httpcaddyfile/directives.go
@@ -37,6 +37,7 @@ var directiveOrder = []string{
 	"uri_replace",
 	"try_files",
 
+	// middleware handlers that typically wrap responses
 	"basicauth",
 	"header",
 	"request_header",
@@ -46,6 +47,7 @@ var directiveOrder = []string{
 	"handle",
 	"route",
 
+	// handlers that typically respond to requests
 	"respond",
 	"reverse_proxy",
 	"php_fastcgi",
@@ -291,6 +293,37 @@ func sortRoutes(routes []ConfigValue) {
 	})
 }
 
+// parseSegmentAsSubroute parses the segment such that its subdirectives
+// are themselves treated as directives, from which a subroute is built
+// and returned.
+func parseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
+	var allResults []ConfigValue
+	for h.Next() {
+		for nesting := h.Nesting(); h.NextBlock(nesting); {
+			dir := h.Val()
+
+			dirFunc, ok := registeredDirectives[dir]
+			if !ok {
+				return nil, h.Errf("unrecognized directive: %s", dir)
+			}
+
+			subHelper := h
+			subHelper.Dispenser = h.NewFromNextSegment()
+
+			results, err := dirFunc(subHelper)
+			if err != nil {
+				return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
+			}
+			for _, result := range results {
+				result.directive = dir
+				allResults = append(allResults, result)
+			}
+		}
+		return buildSubroute(allResults, h.groupCounter) // TODO:  should we move this outside the loop?
+	}
+	return nil, nil
+}
+
 // serverBlock pairs a Caddyfile server block
 // with a "pile" of config values, keyed by class
 // name.
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index f93b8c5cd..7c1379422 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -474,18 +474,18 @@ func (st *ServerType) serversFromPairings(
 				return nil, err
 			}
 
-			if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
-				// no need to wrap the handlers in a subroute if this is
-				// the only server block and there is no matcher for it
-				srv.Routes = append(srv.Routes, siteSubroute.Routes...)
-			} else {
-				srv.Routes = append(srv.Routes, caddyhttp.Route{
-					MatcherSetsRaw: matcherSetsEnc,
-					HandlersRaw: []json.RawMessage{
-						caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings),
-					},
-					Terminal: true, // only first matching site block should be evaluated
-				})
+			// add the site block's route(s) to the server
+			srv.Routes = appendSubrouteToRouteList(srv.Routes, siteSubroute, matcherSetsEnc, p, warnings)
+
+			// if error routes are defined, add those too
+			if errorSubrouteVals, ok := sblock.pile["error_route"]; ok {
+				if srv.Errors == nil {
+					srv.Errors = new(caddyhttp.HTTPErrorConfig)
+				}
+				for _, val := range errorSubrouteVals {
+					sr := val.Value.(*caddyhttp.Subroute)
+					srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings)
+				}
 			}
 		}
 
@@ -497,6 +497,31 @@ func (st *ServerType) serversFromPairings(
 	return servers, nil
 }
 
+// appendSubrouteToRouteList appends the routes in subroute
+// to the routeList, optionally qualified by matchers.
+func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
+	subroute *caddyhttp.Subroute,
+	matcherSetsEnc []caddy.ModuleMap,
+	p sbAddrAssociation,
+	warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
+	if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
+		// no need to wrap the handlers in a subroute if this is
+		// the only server block and there is no matcher for it
+		routeList = append(routeList, subroute.Routes...)
+	} else {
+		routeList = append(routeList, caddyhttp.Route{
+			MatcherSetsRaw: matcherSetsEnc,
+			HandlersRaw: []json.RawMessage{
+				caddyconfig.JSONModuleObject(subroute, "handler", "subroute", warnings),
+			},
+			Terminal: true, // only first matching site block should be evaluated
+		})
+	}
+	return routeList
+}
+
+// buildSubroute turns the config values, which are expected to be routes
+// into a clean and orderly subroute that has all the routes within it.
 func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) {
 	for _, val := range routes {
 		if !directiveIsOrdered(val.directive) {