diff --git a/errors.go b/errors.go
index 8b2b84f..318358c 100644
--- a/errors.go
+++ b/errors.go
@@ -123,13 +123,11 @@ type Error interface {
 	RootCause() error
 }
 
-type structured struct {
-	id        uint64
+type baseError struct {
+	errID     uint64
 	hiddenID  string
 	data      context.Map
 	context   context.Map
-	wrapped   error
-	cause     Error
 	callStack stack.CallStack
 }
 
@@ -142,16 +140,24 @@ func New(desc string, args ...interface{}) Error {
 // NewOffset is like New but offsets the stack by the given offset. This is
 // useful for utilities like golog that may create errors on behalf of others.
 func NewOffset(offset int, desc string, args ...interface{}) Error {
-	var cause error
+	e := buildError(desc, fmt.Sprintf(desc, args...))
+	e.attachStack(2 + offset)
 	for _, arg := range args {
-		err, isError := arg.(error)
+		wrapped, isError := arg.(error)
 		if isError {
-			cause = err
-			break
+			op, _, _, extraData := parseError(wrapped)
+			if op != "" {
+				e.Op(op)
+			}
+			for k, v := range extraData {
+				e.data[k] = v
+			}
+			we := &wrappingError{e, wrapped}
+			bufferError(we)
+			return we
 		}
 	}
-	e := buildError(desc, fmt.Sprintf(desc, args...), nil, Wrap(cause))
-	e.attachStack(2 + offset)
+	bufferError(e)
 	return e
 }
 
@@ -160,33 +166,57 @@ func NewOffset(offset int, desc string, args ...interface{}) Error {
 // errors.Wrap(s.l.Close()) regardless there's an error or not. If the error is
 // already wrapped, it is returned as is.
 func Wrap(err error) Error {
-	return wrapSkipFrames(err, 1)
+	if err == nil {
+		return nil
+	}
+	if e, ok := err.(Error); ok {
+		return e
+	}
+
+	op, goType, desc, extraData := parseError(err)
+	if desc == "" {
+		desc = err.Error()
+	}
+	e := buildError(desc, desc)
+	e.attachStack(2)
+	if op != "" {
+		e.Op(op)
+	}
+	e.data["error_type"] = goType
+	for k, v := range extraData {
+		e.data[k] = v
+	}
+	if cause := getCause(err); cause != nil {
+		we := &wrappingError{e, cause}
+		bufferError(we)
+		return we
+	}
+	bufferError(e)
+	return e
 }
 
 // Fill implements the method from the context.Contextual interface.
-func (e *structured) Fill(m context.Map) {
-	if e != nil {
-		if e.cause != nil {
-			// Include data from cause, which supercedes context
-			e.cause.Fill(m)
-		}
-		// Include the context, which supercedes the cause
-		for key, value := range e.context {
-			m[key] = value
-		}
-		// Now include the error's data, which supercedes everything
-		for key, value := range e.data {
-			m[key] = value
-		}
+func (e *baseError) Fill(m context.Map) {
+	if e == nil {
+		return
+	}
+
+	// Include the context, which supercedes the cause
+	for key, value := range e.context {
+		m[key] = value
+	}
+	// Now include the error's data, which supercedes everything
+	for key, value := range e.data {
+		m[key] = value
 	}
 }
 
-func (e *structured) Op(op string) Error {
+func (e *baseError) Op(op string) Error {
 	e.data["error_op"] = op
 	return e
 }
 
-func (e *structured) With(key string, value interface{}) Error {
+func (e *baseError) With(key string, value interface{}) Error {
 	parts := strings.FieldsFunc(key, func(c rune) bool {
 		return !unicode.IsLetter(c) && !unicode.IsNumber(c)
 	})
@@ -204,126 +234,62 @@ func (e *structured) With(key string, value interface{}) Error {
 	return e
 }
 
-func (e *structured) RootCause() error {
-	if e.cause == nil {
-		if e.wrapped != nil {
-			return e.wrapped
-		}
-		return e
-	}
-	return e.cause.RootCause()
+func (e *baseError) RootCause() error {
+	return e
 }
 
-func (e *structured) ErrorClean() string {
+func (e *baseError) ErrorClean() string {
 	return e.data["error"].(string)
 }
 
 // Error satisfies the error interface
-func (e *structured) Error() string {
+func (e *baseError) Error() string {
 	return e.data["error_text"].(string) + e.hiddenID
 }
 
-func (e *structured) MultiLinePrinter() func(buf *bytes.Buffer) bool {
-	first := true
-	indent := false
-	err := e
+func (e *baseError) MultiLinePrinter() func(*bytes.Buffer) bool {
+	return e.topLevelPrinter()
+}
+
+func (e *baseError) topLevelPrinter() func(*bytes.Buffer) bool {
+	printingStack := false
 	stackPosition := 0
-	switchedCause := false
 	return func(buf *bytes.Buffer) bool {
-		if indent {
-			buf.WriteString("  ")
-		}
-		if first {
+		if !printingStack {
 			buf.WriteString(e.Error())
-			first = false
-			indent = true
-			return true
-		}
-		if switchedCause {
-			fmt.Fprintf(buf, "Caused by: %v", err)
-			if err.callStack != nil && len(err.callStack) > 0 {
-				switchedCause = false
-				indent = true
-				return true
-			}
-			if err.cause == nil {
-				return false
-			}
-			err = err.cause.(*structured)
-			return true
-		}
-		if stackPosition < len(err.callStack) {
-			buf.WriteString("at ")
-			call := err.callStack[stackPosition]
-			fmt.Fprintf(buf, "%+n (%s:%d)", call, call, call)
-			stackPosition++
-		}
-		if stackPosition >= len(err.callStack) {
-			switch cause := err.cause.(type) {
-			case *structured:
-				err = cause
-				indent = false
-				stackPosition = 0
-				switchedCause = true
-			default:
-				return false
-			}
+			printingStack = true
+			return len(e.callStack) > 0
 		}
-		return err != nil
+		call := e.callStack[stackPosition]
+		fmt.Fprintf(buf, "  at %+n (%s:%d)", call, call, call)
+		stackPosition++
+		return stackPosition < len(e.callStack)
 	}
 }
 
-func wrapSkipFrames(err error, skip int) Error {
-	if err == nil {
-		return nil
-	}
-
-	// Look for *structureds
-	if e, ok := err.(*structured); ok {
-		return e
-	}
+func (e *baseError) attachStack(skip int) {
+	call := stack.Caller(skip)
+	e.callStack = stack.Trace().TrimBelow(call)
+	e.data["error_location"] = fmt.Sprintf("%+n (%s:%d)", call, call, call)
+}
 
-	var cause Error
-	// Look for hidden *structureds
-	hiddenIDs, err2 := hidden.Extract(err.Error())
-	if err2 == nil && len(hiddenIDs) > 0 {
-		// Take the first hidden ID as our cause
-		cause = get(hiddenIDs[0])
-	}
+func (e *baseError) id() uint64 {
+	return e.errID
+}
 
-	// Create a new *structured
-	return buildError("", "", err, cause)
+func (e *baseError) setID(id uint64) {
+	e.errID = id
 }
 
-func (e *structured) attachStack(skip int) {
-	call := stack.Caller(skip)
-	e.callStack = stack.Trace().TrimBelow(call)
-	e.data["error_location"] = fmt.Sprintf("%+n (%s:%d)", call, call, call)
+func (e *baseError) setHiddenID(id string) {
+	e.hiddenID = id
 }
 
-func buildError(desc string, fullText string, wrapped error, cause Error) *structured {
-	e := &structured{
+func buildError(desc string, fullText string) *baseError {
+	e := &baseError{
 		data: make(context.Map),
 		// We capture the current context to allow it to propagate to higher layers.
 		context: ops.AsMap(nil, false),
-		wrapped: wrapped,
-		cause:   cause,
-	}
-	e.save()
-
-	errorType := "errors.Error"
-	if wrapped != nil {
-		op, goType, wrappedDesc, extra := parseError(wrapped)
-		if desc == "" {
-			desc = wrappedDesc
-		}
-		e.Op(op)
-		errorType = goType
-		if extra != nil {
-			for key, value := range extra {
-				e.data[key] = value
-			}
-		}
 	}
 
 	cleanedDesc := hidden.Clean(desc)
@@ -333,11 +299,125 @@ func buildError(desc string, fullText string, wrapped error, cause Error) *struc
 	} else {
 		e.data["error_text"] = cleanedDesc
 	}
-	e.data["error_type"] = errorType
+	e.data["error_type"] = "errors.Error"
+
+	return e
+}
+
+type topLevelPrinter interface {
+	// Returns a printer which prints only the top-level error and any associated stack trace. The
+	// output of this printer will be a prefix of the output from MultiLinePrinter().
+	topLevelPrinter() func(*bytes.Buffer) bool
+}
+
+type unwrapper interface {
+	Unwrap() error
+}
+
+type wrappingError struct {
+	*baseError
+	wrapped error
+}
+
+// Implements error unwrapping as described in the standard library's errors package:
+// https://golang.org/pkg/errors/#pkg-overview
+func (e *wrappingError) Unwrap() error {
+	return e.wrapped
+}
+
+func (e *wrappingError) Fill(m context.Map) {
+	type filler interface{ Fill(context.Map) }
+
+	applyToChain(e.wrapped, func(err error) {
+		if f, ok := err.(filler); ok {
+			f.Fill(m)
+		}
+	})
+	e.baseError.Fill(m)
+}
+
+func (e *wrappingError) RootCause() error {
+	return unwrapToRoot(e)
+}
+
+func (e *wrappingError) MultiLinePrinter() func(*bytes.Buffer) bool {
+	var (
+		currentPrinter = e.baseError.topLevelPrinter()
+		nextErr        = e.wrapped
+		prefix         = ""
+	)
+	return func(buf *bytes.Buffer) bool {
+		fmt.Fprint(buf, prefix)
+		if currentPrinter(buf) {
+			prefix = ""
+			return true
+		}
+		if nextErr == nil {
+			return false
+		}
+		currentPrinter = getTopLevelPrinter(nextErr)
+		prefix = "Caused by: "
+		if uw, ok := nextErr.(unwrapper); ok {
+			nextErr = uw.Unwrap()
+		} else {
+			nextErr = nil
+		}
+		return true
+	}
+}
+
+// We have to implement these two methods or the fluid syntax will result in the embedded *baseError
+// being returned, not the *wrappingError.
 
+func (e *wrappingError) Op(op string) Error {
+	e.baseError = e.baseError.Op(op).(*baseError)
 	return e
 }
 
+func (e *wrappingError) With(key string, value interface{}) Error {
+	e.baseError = e.baseError.With(key, value).(*baseError)
+	return e
+}
+
+func getTopLevelPrinter(err error) func(*bytes.Buffer) bool {
+	if tlp, ok := err.(topLevelPrinter); ok {
+		return tlp.topLevelPrinter()
+	}
+	return func(buf *bytes.Buffer) bool {
+		fmt.Fprint(buf, err)
+		return false
+	}
+}
+
+func getCause(e error) error {
+	if uw, ok := e.(unwrapper); ok {
+		return uw.Unwrap()
+	}
+	// Look for hidden *baseErrors
+	hiddenIDs, extractErr := hidden.Extract(e.Error())
+	if extractErr == nil && len(hiddenIDs) > 0 {
+		// Take the first hidden ID as our cause
+		return get(hiddenIDs[0])
+	}
+	return nil
+}
+
+func unwrapToRoot(e error) error {
+	if uw, ok := e.(unwrapper); ok {
+		return unwrapToRoot(uw.Unwrap())
+	}
+	return e
+}
+
+// Applies f to the chain of errors unwrapped from err. The function is applied to the root cause
+// first and err last.
+func applyToChain(err error, f func(error)) {
+	if uw, ok := err.(unwrapper); ok {
+		applyToChain(uw.Unwrap(), f)
+	}
+	f(err)
+}
+
 func parseError(err error) (op string, goType string, desc string, extra map[string]string) {
 	extra = make(map[string]string)
 
diff --git a/errors_test.go b/errors_test.go
index 7c4887a..effee34 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -2,7 +2,10 @@ package errors
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
+	"io"
+	"net"
 	"regexp"
 	"testing"
 
@@ -10,6 +13,7 @@ import (
 	"github.com/getlantern/hidden"
 	"github.com/getlantern/ops"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 var (
@@ -29,9 +33,10 @@ func TestFull(t *testing.T) {
 		}
 		assert.Equal(t, "Hello There", e.Error()[:11])
 		op = ops.Begin("op2").Set("ca", 200).Set("cb", 200).Set("cc", 200)
-		e3 := Wrap(fmt.Errorf("I'm wrapping your text: %v", e)).Op("outer op").With("dATA+1", i).With("cb", 300)
+		e3 := Wrap(fmt.Errorf("I'm wrapping your text: %w", e)).Op("outer op").With("dATA+1", i).With("cb", 300)
 		op.End()
-		assert.Equal(t, e, e3.(*structured).cause, "Wrapping a regular error should have extracted the contained *Error")
+		require.IsType(t, (*wrappingError)(nil), e3, "wrapping an error with cause should have resulted in a *wrappingError")
+		assert.Equal(t, e, e3.(*wrappingError).wrapped, "Wrapping a regular error should have extracted the contained *Error")
 		m := make(context.Map)
 		e3.Fill(m)
 		assert.Equal(t, i, m["data_1"], "Error's data should dominate all")
@@ -39,26 +44,27 @@ func TestFull(t *testing.T) {
 		assert.Equal(t, 300, m["cb"], "Error's data should dominate its context")
 		assert.Equal(t, 200, m["cc"], "Error's context should come through")
 		assert.Equal(t, 100, m["cd"], "Cause's context should come through")
-		assert.Equal(t, "My Op", e.(*structured).data["error_op"], "Op should be available from cause")
+		assert.Equal(t, "My Op", e.(*baseError).data["error_op"], "Op should be available from cause")
 
-		for _, call := range e3.(*structured).callStack {
+		for _, call := range e3.(*wrappingError).callStack {
 			t.Logf("at %v", call)
 		}
 	}
 
 	e3 := Wrap(fmt.Errorf("I'm wrapping your text: %v", firstErr)).With("a", 2)
-	assert.Nil(t, e3.(*structured).cause, "Wrapping an *Error that's no longer buffered should have yielded no cause")
+	require.IsType(t, (*baseError)(nil), e3, "Wrapping an *Error that's no longer buffered should have resulted in a *baseError")
 }
 
 func TestNewWithCause(t *testing.T) {
 	cause := buildCause()
 	outer := New("Hello %v", cause)
 	assert.Equal(t, "Hello World", hidden.Clean(outer.Error()))
-	assert.Equal(t, "Hello %v", outer.(*structured).ErrorClean())
+	assert.Equal(t, "Hello %v", outer.ErrorClean())
+	require.IsType(t, (*wrappingError)(nil), outer, "Including an error arg should have resulted in a *wrappingError")
 	assert.Equal(t,
 		"github.com/getlantern/errors.TestNewWithCause (errors_test.go:999)",
-		replaceNumbers.ReplaceAllString(outer.(*structured).data["error_location"].(string), "999"))
-	assert.Equal(t, cause, outer.(*structured).cause)
+		replaceNumbers.ReplaceAllString(outer.(*wrappingError).data["error_location"].(string), "999"))
+	assert.Equal(t, cause, outer.(*wrappingError).wrapped)
 
 	// Make sure that stacktrace prints out okay
 	buf := &bytes.Buffer{}
@@ -99,7 +105,7 @@ func buildCause() Error {
 }
 
 func buildSubCause() error {
-	return fmt.Errorf("or%v", buildSubSubCause())
+	return fmt.Errorf("or%w", buildSubSubCause())
 }
 
 func buildSubSubCause() error {
@@ -123,7 +129,7 @@ func TestHiddenWithCause(t *testing.T) {
 	e2 := New("I wrap: %v", e1)
 	e3 := fmt.Errorf("Hiding %v", e2)
 	// clear hidden buffer
-	hiddenErrors = make([]*structured, 100)
+	hiddenErrors = make([]hideableError, 100)
 	e4 := Wrap(e3)
 	e5 := New("I'm really outer: %v", e4)
 
@@ -136,7 +142,54 @@ func TestHiddenWithCause(t *testing.T) {
 			break
 		}
 	}
-	fmt.Println(buf.String())
 	// We're not asserting the output because we're just making sure that printing
 	// doesn't panic. If we get to this point without panicking, we're happy.
 }
+
+func TestFill(t *testing.T) {
+	e := New("something happened").(*baseError)
+	e2 := New("uh oh: %v", e).(*wrappingError)
+	e3 := fmt.Errorf("hmm: %w", e2)
+	e4 := New("umm: %v", e3).(*wrappingError)
+
+	e4.data["name"] = "e4"
+	e2.data["name"] = "e2"
+	e.data["name"] = "e"
+	e2.data["k"] = "v2"
+	e.data["k"] = "v"
+	e.data["a"] = "b"
+
+	m := context.Map{}
+	e4.Fill(m)
+	require.Equal(t, "e4", m["name"])
+	require.Equal(t, "v2", m["k"])
+	require.Equal(t, "b", m["a"])
+}
+
+// Ensures that this package implements error unwrapping as described in:
+// https://golang.org/pkg/errors/#pkg-overview
+func TestUnwrapping(t *testing.T) {
+	sampleUnwrapper := fmt.Errorf("%w", fmt.Errorf("something happened"))
+
+	errNoCause := New("something happened")
+	_, ok := errNoCause.(unwrapper)
+	assert.False(t, ok, "error with no cause should not implement Unwrap method")
+	wrappedNoCause := Wrap(errNoCause)
+	_, ok = wrappedNoCause.(unwrapper)
+	assert.False(t, ok, "wrapped error with no cause should not implement Unwrap method")
+
+	errFromEOF := New("something happened: %v", io.EOF)
+	assert.Implements(t, &sampleUnwrapper, errFromEOF)
+	assert.True(t, errors.Is(errFromEOF, io.EOF))
+	wrappedFromEOF := Wrap(errFromEOF)
+	assert.Implements(t, &sampleUnwrapper, wrappedFromEOF)
+	assert.True(t, errors.Is(wrappedFromEOF, io.EOF))
+
+	addrErrHolder := new(net.AddrError)
+	errFromAddrErr := New("something happend: %v", new(net.AddrError))
+	assert.Implements(t, &sampleUnwrapper, errFromAddrErr)
+	assert.True(t, errors.As(errFromAddrErr, &addrErrHolder))
+	wrappedFromAddrErr := Wrap(errFromAddrErr)
+	assert.Implements(t, &sampleUnwrapper, wrappedFromAddrErr)
+	assert.True(t, errors.As(wrappedFromAddrErr, &addrErrHolder))
+}
diff --git a/hide.go b/hide.go
index f10d863..6dbd410 100644
--- a/hide.go
+++ b/hide.go
@@ -8,22 +8,29 @@ import (
 )
 
 var (
-	hiddenErrors = make([]*structured, 100)
+	hiddenErrors = make([]hideableError, 100)
 	nextID       = uint64(0)
 	hiddenMutex  sync.RWMutex
 )
 
+type hideableError interface {
+	Error
+	id() uint64
+	setID(uint64)
+	setHiddenID(string)
+}
+
 // This trick saves the error to a ring buffer and embeds a non-printing
-// hiddenID in the error's description, so that if the errors is later wrapped
+// hiddenID in the error's description, so that if the error is later wrapped
 // by a standard error using something like
 // fmt.Errorf("An error occurred: %v", thisError), we can subsequently extract
 // the error simply using the hiddenID in the string.
-func (e *structured) save() {
+func bufferError(e hideableError) {
 	hiddenMutex.Lock()
 	b := make([]byte, 8)
 	binary.BigEndian.PutUint64(b, nextID)
-	e.id = nextID
-	e.hiddenID = hidden.ToString(b)
+	e.setID(nextID)
+	e.setHiddenID(hidden.ToString(b))
 	hiddenErrors[idxForID(nextID)] = e
 	nextID++
 	hiddenMutex.Unlock()
@@ -37,7 +44,7 @@ func get(hiddenID []byte) Error {
 	hiddenMutex.RLock()
 	err := hiddenErrors[idxForID(id)]
 	hiddenMutex.RUnlock()
-	if err != nil && err.id == id {
+	if err != nil && err.id() == id {
 		// Found it!
 		return err
 	}