Repository: gohugoio/hugo (language: go)

Issue:
Replace error handling in GetRemote with try
See #9797

Unfortunately this is hard to deprecate. Fortunately the breakage will be loud and should be simple to fix. So for the example in docs:

```handlebars
{{ $url := "https://example.org/images/a.jpg" }}
{{ with resources.GetRemote $url }}
  {{ with .Err }}
    {{ errorf "%s" . }}
  {{ else }}
    <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
  {{ end }}
{{ else }}
  {{ errorf "Unable to get remote resource %q" $url }}
{{ end }}
```

Will need to be rewritten to:

```handlebars
{{ $url := "https://example.org/images/a.jpg" }}
{{ with try (resources.GetRemote $url) }}
  {{ with .Err }}
    {{ errorf "%s" . }}
  {{ else with .Value }}
    <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
  {{ else }}
      {{ errorf "Unable to get remote resource %q" $url }}
  {{ end }}
{{ end }}
```




Discussion / hints:

Fix patch:
diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go
index c8a48823e82..38b1986568b 100644
--- a/common/herrors/file_error.go
+++ b/common/herrors/file_error.go
@@ -258,8 +258,27 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
 	return f, realFilename, nil
 }
 
-// Cause returns the underlying error or itself if it does not implement Unwrap.
+// Cause returns the underlying error, that is,
+// it unwraps errors until it finds one that does not implement
+// the Unwrap method.
+// For a shallow variant, see Unwrap.
 func Cause(err error) error {
+	type unwrapper interface {
+		Unwrap() error
+	}
+
+	for err != nil {
+		cause, ok := err.(unwrapper)
+		if !ok {
+			break
+		}
+		err = cause.Unwrap()
+	}
+	return err
+}
+
+// Unwrap returns the underlying error or itself if it does not implement Unwrap.
+func Unwrap(err error) error {
 	if u := errors.Unwrap(err); u != nil {
 		return u
 	}
@@ -267,7 +286,7 @@ func Cause(err error) error {
 }
 
 func extractFileTypePos(err error) (string, text.Position) {
-	err = Cause(err)
+	err = Unwrap(err)
 
 	var fileType string
 
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index 02ecd57855d..60fb8ecc02f 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -343,6 +343,18 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error {
 
 	siteRenderContext := &siteRenderContext{cfg: config, multihost: h.Configs.IsMultihost}
 
+	renderErr := func(err error) error {
+		if err == nil {
+			return nil
+		}
+		if strings.Contains(err.Error(), "can't evaluate field Err in type resource.Resource") {
+			// In Hugo 0.141.0 we replaced the special error handling for resources.GetRemote
+			// with the more general try.
+			return fmt.Errorf("%s: Resource.Err was removed in Hugo v0.141.0 and replaced with a new try keyword, see https://gohugo.io/functions/go-template/try/", err)
+		}
+		return err
+	}
+
 	i := 0
 	for _, s := range h.Sites {
 		segmentFilter := s.conf.C.SegmentFilter
@@ -390,7 +402,7 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error {
 							}
 						} else {
 							if err := s.render(siteRenderContext); err != nil {
-								return err
+								return renderErr(err)
 							}
 						}
 						loggers.TimeTrackf(ll, start, nil, "")
diff --git a/hugolib/page.go b/hugolib/page.go
index 83f0c6e25c2..3653767379a 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -185,10 +185,6 @@ func (p *pageState) isContentNodeBranch() bool {
 	return p.IsNode()
 }
 
-func (p *pageState) Err() resource.ResourceError {
-	return nil
-}
-
 // Eq returns whether the current page equals the given page.
 // This is what's invoked when doing `{{ if eq $page $otherPage }}`
 func (p *pageState) Eq(other any) bool {
diff --git a/resources/errorResource.go b/resources/errorResource.go
deleted file mode 100644
index 582c54f6d15..00000000000
--- a/resources/errorResource.go
+++ /dev/null
@@ -1,145 +0,0 @@
-// Copyright 2021 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package resources
-
-import (
-	"context"
-	"image"
-
-	"github.com/gohugoio/hugo/common/hugio"
-	"github.com/gohugoio/hugo/common/maps"
-	"github.com/gohugoio/hugo/media"
-	"github.com/gohugoio/hugo/resources/images"
-	"github.com/gohugoio/hugo/resources/images/exif"
-	"github.com/gohugoio/hugo/resources/resource"
-)
-
-var (
-	_ error = (*errorResource)(nil)
-	// Image covers all current Resource implementations.
-	_ images.ImageResource = (*errorResource)(nil)
-	// The list of user facing and exported interfaces in resource.go
-	// Note that if we're missing some interface here, the user will still
-	// get an error, but not as pretty.
-	_ resource.ContentResource         = (*errorResource)(nil)
-	_ resource.ReadSeekCloserResource  = (*errorResource)(nil)
-	_ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
-	// Make sure it also fails when passed to a pipe function.
-	_ ResourceTransformer = (*errorResource)(nil)
-)
-
-// NewErrorResource wraps err in a Resource where all but the Err method will panic.
-func NewErrorResource(err resource.ResourceError) resource.Resource {
-	return &errorResource{ResourceError: err}
-}
-
-type errorResource struct {
-	resource.ResourceError
-}
-
-func (e *errorResource) Err() resource.ResourceError {
-	return e.ResourceError
-}
-
-func (e *errorResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Content(context.Context) (any, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) ResourceType() string {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) MediaType() media.Type {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Permalink() string {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) RelPermalink() string {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Name() string {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Title() string {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Params() maps.Params {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Data() any {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Height() int {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Width() int {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Process(spec string) (images.ImageResource, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Crop(spec string) (images.ImageResource, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Fill(spec string) (images.ImageResource, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Fit(spec string) (images.ImageResource, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Resize(spec string) (images.ImageResource, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Filter(filters ...any) (images.ImageResource, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Exif() *exif.ExifInfo {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Colors() ([]images.Color, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) DecodeImage() (image.Image, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) Transform(...ResourceTransformation) (ResourceTransformer, error) {
-	panic(e.ResourceError)
-}
-
-func (e *errorResource) TransformWithContext(context.Context, ...ResourceTransformation) (ResourceTransformer, error) {
-	panic(e.ResourceError)
-}
diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go
index 5a03b19941c..af9f2682d37 100644
--- a/resources/page/page_nop.go
+++ b/resources/page/page_nop.go
@@ -61,10 +61,6 @@ type nopPage int
 
 var noOpPathInfo = media.DefaultPathParser.Parse(files.ComponentFolderContent, "no-op.md")
 
-func (p *nopPage) Err() resource.ResourceError {
-	return nil
-}
-
 func (p *nopPage) Aliases() []string {
 	return nil
 }
diff --git a/resources/resource.go b/resources/resource.go
index 7ab10b0ae34..29b9e5ddd82 100644
--- a/resources/resource.go
+++ b/resources/resource.go
@@ -224,9 +224,6 @@ type resourceCopier interface {
 
 // Copy copies r to the targetPath given.
 func Copy(r resource.Resource, targetPath string) resource.Resource {
-	if r.Err() != nil {
-		panic(fmt.Sprintf("Resource has an .Err: %s", r.Err()))
-	}
 	return r.(resourceCopier).cloneTo(targetPath)
 }
 
@@ -439,10 +436,6 @@ func (l *genericResource) Content(context.Context) (any, error) {
 	return hugio.ReadString(r)
 }
 
-func (r *genericResource) Err() resource.ResourceError {
-	return nil
-}
-
 func (l *genericResource) Data() any {
 	return l.sd.Data
 }
diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go
index b33750e8033..585dfd1505e 100644
--- a/resources/resource/resourcetypes.go
+++ b/resources/resource/resourcetypes.go
@@ -43,6 +43,9 @@ type OriginProvider interface {
 
 // NewResourceError creates a new ResourceError.
 func NewResourceError(err error, data any) ResourceError {
+	if data == nil {
+		data = map[string]any{}
+	}
 	return &resourceError{
 		error: err,
 		data:  data,
@@ -65,13 +68,6 @@ type ResourceError interface {
 	ResourceDataProvider
 }
 
-// ErrProvider provides an Err.
-type ErrProvider interface {
-	// Err returns an error if this resource is in an error state.
-	// This will currently only be set for resources obtained from resources.GetRemote.
-	Err() ResourceError
-}
-
 // Resource represents a linkable resource, i.e. a content page, image etc.
 type Resource interface {
 	ResourceWithoutMeta
@@ -83,7 +79,6 @@ type ResourceWithoutMeta interface {
 	MediaTypeProvider
 	ResourceLinksProvider
 	ResourceDataProvider
-	ErrProvider
 }
 
 type ResourceWrapper interface {
diff --git a/resources/transform.go b/resources/transform.go
index c5d24066937..73f3b85d25d 100644
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -192,10 +192,6 @@ func (r *resourceAdapter) Content(ctx context.Context) (any, error) {
 	return r.target.Content(ctx)
 }
 
-func (r *resourceAdapter) Err() resource.ResourceError {
-	return nil
-}
-
 func (r *resourceAdapter) GetIdentity() identity.Identity {
 	return identity.FirstIdentity(r.target)
 }
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
index 0dbee02f733..36962c444b8 100644
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -19,6 +19,7 @@ import (
 	"io"
 	"reflect"
 
+	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/common/hreflect"
 
 	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
@@ -256,14 +257,34 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
 	panic("not reached")
 }
 
+// newErrorWithCause creates a new error with the given cause.
+func newErrorWithCause(err error) *TryError {
+	return &TryError{Err: err, Cause: herrors.Cause(err)}
+}
+
+// TryError wraps an error with a cause.
+type TryError struct {
+	Err   error
+	Cause error
+}
+
+func (e *TryError) Error() string {
+	return e.Err.Error()
+}
+
+func (e *TryError) Unwrap() error {
+	return e.Err
+}
+
 // TryValue is what gets returned when using the "try" keyword.
 type TryValue struct {
 	// Value is the value returned by the function or method wrapped with "try".
 	// This will always be nil if Err is set.
 	Value any
+
 	// Err is the error returned by the function or method wrapped with "try".
 	// This will always be nil if Value is set.
-	Err error
+	Err *TryError
 }
 
 // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
@@ -274,10 +295,11 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
 	if name == "try" {
 		defer func() {
 			if r := recover(); r != nil {
+				// Cause: herrors.Cause(err)
 				if err, ok := r.(error); ok {
-					val = reflect.ValueOf(TryValue{nil, err})
+					val = reflect.ValueOf(TryValue{Value: nil, Err: newErrorWithCause(err)})
 				} else {
-					val = reflect.ValueOf(TryValue{nil, fmt.Errorf("%v", r)})
+					val = reflect.ValueOf(TryValue{Value: nil, Err: newErrorWithCause(fmt.Errorf("%v", r))})
 				}
 			}
 		}()
@@ -396,7 +418,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
 
 	// Added for Hugo.
 	if name == "try" {
-		return reflect.ValueOf(TryValue{vv.Interface(), nil})
+		return reflect.ValueOf(TryValue{Value: vv.Interface()})
 	}
 
 	return vv
diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go
index beace14e6c8..f28cc36fe07 100644
--- a/tpl/resources/resources.go
+++ b/tpl/resources/resources.go
@@ -115,14 +115,10 @@ func (ns *Namespace) Get(filename any) resource.Resource {
 //
 // Note: This method does not return any error as a second return value,
 // for any error situations the error can be checked in .Err.
-func (ns *Namespace) GetRemote(args ...any) resource.Resource {
+func (ns *Namespace) GetRemote(args ...any) (resource.Resource, error) {
 	get := func(args ...any) (resource.Resource, error) {
-		if len(args) < 1 {
-			return nil, errors.New("must provide an URL")
-		}
-
-		if len(args) > 2 {
-			return nil, errors.New("must not provide more arguments than URL and options")
+		if len(args) < 1 || len(args) > 2 {
+			return nil, errors.New("must provide an URL and optionally an options map")
 		}
 
 		urlstr, err := cast.ToStringE(args[0])
@@ -146,12 +142,12 @@ func (ns *Namespace) GetRemote(args ...any) resource.Resource {
 	if err != nil {
 		switch v := err.(type) {
 		case *create.HTTPError:
-			return resources.NewErrorResource(resource.NewResourceError(v, v.Data))
+			return nil, resource.NewResourceError(v, v.Data)
 		default:
-			return resources.NewErrorResource(resource.NewResourceError(fmt.Errorf("error calling resources.GetRemote: %w", err), make(map[string]any)))
+			return nil, resource.NewResourceError(err, nil)
 		}
 	}
-	return r
+	return r, nil
 }
 
 // GetMatch finds the first Resource matching the given pattern, or nil if none found.
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html b/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
index ba5a851eeb3..b88cf7ce0ea 100644
--- a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
+++ b/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
@@ -17,13 +17,13 @@
   {{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
   {{- $query := querify "url" $url "dnt" .dnt -}}
   {{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
-  {{- with resources.GetRemote $request -}}
+  {{- with try (resources.GetRemote $request) -}}
     {{- with .Err -}}
       {{- errorf "%s" . -}}
-    {{- else -}}
+    {{- else with .Value -}}
       {{- (. | transform.Unmarshal).html | safeHTML -}}
-    {{- end -}}
-  {{- else -}}
+    {{- else -}}
     {{- warnidf "shortcode-twitter-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" .name .position -}}
+    {{- end -}}
   {{- end -}}
 {{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html
index 1f3b3c523ea..0fc8613b938 100644
--- a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html
+++ b/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html
@@ -14,14 +14,14 @@
   {{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
   {{- $query := querify "url" $url "dnt" .dnt "omit_script" true -}}
   {{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
-  {{- with resources.GetRemote $request -}}
+  {{- with try (resources.GetRemote $request) -}}
     {{- with .Err -}}
       {{- errorf "%s" . -}}
-    {{- else -}}
+    {{- else with .Value -}}
       {{- (. | transform.Unmarshal).html | safeHTML -}}
+    {{- else -}}
+      {{- warnidf "shortcode-twitter-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" .name .position -}}
     {{- end -}}
-  {{- else -}}
-    {{- warnidf "shortcode-twitter-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" .name .position -}}
   {{- end -}}
 {{- end -}}
 
@@ -31,7 +31,16 @@
     {{- .Page.Scratch.Set "__h_simple_twitter_css" true -}}
     <style type="text/css">
       .twitter-tweet {
-        font: 14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+        font:
+          14px/1.45 -apple-system,
+          BlinkMacSystemFont,
+          "Segoe UI",
+          Roboto,
+          Oxygen-Sans,
+          Ubuntu,
+          Cantarell,
+          "Helvetica Neue",
+          sans-serif;
         border-left: 4px solid #2b7bb9;
         padding-left: 1.5em;
         color: #555;
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
index e262d3c3cb0..7f7940b804d 100644
--- a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
+++ b/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
@@ -23,10 +23,10 @@
   {{- $url := urls.JoinPath "https://vimeo.com" .id -}}
   {{- $query := querify "url" $url "dnt" $dnt -}}
   {{- $request := printf "https://vimeo.com/api/oembed.json?%s" $query -}}
-  {{- with resources.GetRemote $request -}}
+  {{- with try (resources.GetRemote $request) -}}
     {{- with .Err -}}
       {{- errorf "%s" . -}}
-    {{- else -}}
+    {{- else with .Value -}}
       {{- with . | transform.Unmarshal -}}
         {{- $class := printf "%s %s" "s_video_simple" "__h_video" -}}
         {{- with $.class -}}
@@ -45,8 +45,8 @@
           </a>
         </div>
       {{- end -}}
+    {{- else -}}
+      {{- warnidf "shortcode-vimeo-simple" "The %q shortcode was unable to retrieve the remote data. See %s" .name .position -}}
     {{- end -}}
-  {{- else -}}
-    {{- warnidf "shortcode-vimeo-simple" "The %q shortcode was unable to retrieve the remote data. See %s" .name .position -}}
   {{- end -}}
 {{- end -}}


Test patch:
diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go
index 0b17a8db091..669114c8a4a 100644
--- a/hugolib/resource_chain_test.go
+++ b/hugolib/resource_chain_test.go
@@ -67,11 +67,11 @@ FIT: {{ $fit.Name }}|{{ $fit.RelPermalink }}|{{ $fit.Width }}
 CSS integrity Data first: {{ $cssFingerprinted1.Data.Integrity }} {{ $cssFingerprinted1.RelPermalink }}
 CSS integrity Data last:  {{ $cssFingerprinted2.RelPermalink }} {{ $cssFingerprinted2.Data.Integrity }}
 
-{{ $failedImg := resources.GetRemote "%[1]s/fail.jpg" }}
+{{ $failedImg := try (resources.GetRemote "%[1]s/fail.jpg") }}
 {{ $rimg := resources.GetRemote "%[1]s/sunset.jpg" }}
 {{ $remotenotfound := resources.GetRemote "%[1]s/notfound.jpg" }}
 {{ $localnotfound := resources.Get "images/notfound.jpg" }}
-{{ $gopherprotocol := resources.GetRemote "gopher://example.org" }}
+{{ $gopherprotocol := try (resources.GetRemote "gopher://example.org") }}
 {{ $rfit := $rimg.Fit "200x200" }}
 {{ $rfit2 := $rfit.Fit "100x200" }}
 {{ $rimg = $rimg | fingerprint }}
@@ -79,10 +79,10 @@ SUNSET REMOTE: {{ $rimg.Name }}|{{ $rimg.RelPermalink }}|{{ $rimg.Width }}|{{ le
 FIT REMOTE: {{ $rfit.Name }}|{{ $rfit.RelPermalink }}|{{ $rfit.Width }}
 REMOTE NOT FOUND: {{ if $remotenotfound }}FAILED{{ else}}OK{{ end }}
 LOCAL NOT FOUND: {{ if $localnotfound }}FAILED{{ else}}OK{{ end }}
-PRINT PROTOCOL ERROR1: {{ with $gopherprotocol }}{{ . | safeHTML }}{{ end }}
+PRINT PROTOCOL ERROR1: {{ with $gopherprotocol }}{{ .Value | safeHTML }}{{ end }}
 PRINT PROTOCOL ERROR2: {{ with $gopherprotocol }}{{ .Err | safeHTML }}{{ end }}
-PRINT PROTOCOL ERROR DETAILS: {{ with $gopherprotocol }}Err: {{ .Err | safeHTML }}{{ with .Err }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}{{ end }}|{{ end }}{{ end }}
-FAILED REMOTE ERROR DETAILS CONTENT: {{ with $failedImg.Err }}|{{ . }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}|ContentLength: {{ .ContentLength }}|ContentType: {{ .ContentType }}{{ end }}{{ end }}|
+PRINT PROTOCOL ERROR DETAILS: {{ with $gopherprotocol }}{{ with .Err }}Err: {{ . | safeHTML }}{{ with .Cause }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}{{ end }}|{{ end }}{{ end }}{{ end }}
+FAILED REMOTE ERROR DETAILS CONTENT: {{ with $failedImg }}{{ with .Err }}{{ with .Cause }}{{ . }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}|ContentLength: {{ .ContentLength }}|ContentType: {{ .ContentType }}{{ end }}{{ end }}{{ end }}{{ end }}|
 `, ts.URL))
 
 	fs := b.Fs.Source
@@ -114,8 +114,8 @@ SUNSET REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s.a9bf1d944e19c0f382e0d8f51de690f7d
 FIT REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s_hu15210517121918042184.jpg|200
 REMOTE NOT FOUND: OK
 LOCAL NOT FOUND: OK
-PRINT PROTOCOL ERROR DETAILS: Err: error calling resources.GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"||
-FAILED REMOTE ERROR DETAILS CONTENT: |failed to fetch remote resource from &#39;%[2]s/fail.jpg&#39;: Not Implemented|Body: { msg: failed }
+PRINT PROTOCOL ERROR DETAILS: Err: template: index.html:22:36: executing "index.html" at <resources.GetRemote>: error calling GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"|
+FAILED REMOTE ERROR DETAILS CONTENT: failed to fetch remote resource from &#39;%[2]s/fail.jpg&#39;: Not Implemented|Body: { msg: failed }
 |StatusCode: 501|ContentLength: 16|ContentType: text/plain; charset=utf-8|
 
 
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
index 8a2d28e31fe..8e6dfb79ac2 100644
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -111,10 +111,6 @@ type testPage struct {
 	sectionEntries []string
 }
 
-func (p *testPage) Err() resource.ResourceError {
-	return nil
-}
-
 func (p *testPage) Aliases() []string {
 	panic("testpage: not implemented")
 }
diff --git a/resources/resource_factories/create/create_integration_test.go b/resources/resource_factories/create/create_integration_test.go
index 17084574da9..7b9c96e3497 100644
--- a/resources/resource_factories/create/create_integration_test.go
+++ b/resources/resource_factories/create/create_integration_test.go
@@ -31,18 +31,17 @@ func TestGetRemoteHead(t *testing.T) {
   [security.http]
     methods = ['(?i)GET|POST|HEAD']
     urls = ['.*gohugo\.io.*']
-
 -- layouts/index.html --
 {{ $url := "https://gohugo.io/img/hugo.png" }}
 {{ $opts := dict "method" "head" }}
-{{ with resources.GetRemote $url $opts }}
+{{ with try (resources.GetRemote $url $opts) }}
   {{ with .Err }}
     {{ errorf "Unable to get remote resource: %s" . }}
-  {{ else }}
+  {{ else with .Value }}
     Head Content: {{ .Content }}. Head Data: {{ .Data }}
-  {{ end }}
-{{ else }}
+  {{ else }}
   {{ errorf "Unable to get remote resource: %s" $url }}
+  {{ end }}
 {{ end }}
 `
 
@@ -90,14 +89,15 @@ mediaTypes = ['text/plain']
 -- layouts/_default/single.html --
 {{ $url := printf "%s%s" "URL" .RelPermalink}}
 {{ $opts := dict }}
-{{ with resources.GetRemote $url $opts }}
+{{ with try (resources.GetRemote $url $opts) }}
   {{ with .Err }}
-    {{ errorf "Got Err: %s. Data: %v" . .Data }}
-  {{ else }}
+     {{ errorf "Got Err: %s" . }}
+	 {{ with .Cause }}{{ errorf "Data: %s" .Data }}{{ end }}
+  {{ else with .Value }}
     Content: {{ .Content }}
+  {{ else }}
+    {{ errorf "Unable to get remote resource: %s" $url }}
   {{ end }}
-{{ else }}
-  {{ errorf "Unable to get remote resource: %s" $url }}
 {{ end }}
 `