diff --git a/README.md b/README.md
index c927e8c1b6189c0e3fe13a918f1827f800d97837..477a59ff3d81292dd1ac481d2a715fcd837d1520 100644
--- a/README.md
+++ b/README.md
@@ -16,17 +16,17 @@ go get nhooyr.io/websocket
 ## Features
 
 - Minimal and idiomatic API
-- Tiny codebase at 2200 lines
 - First class [context.Context](https://blog.golang.org/context) support
 - Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite)
 - [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports)
 - JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages
-- Highly optimized by default
-  - Zero alloc reads and writes
-- Concurrent writes out of the box
-- [Complete Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) support
-- [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close)
-- Full support of [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression extension
+- Zero alloc reads and writes
+- Concurrent writes
+- WebSocket [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close)
+- [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper
+- WebSocket [Pings](https://godoc.org/nhooyr.io/websocket#Conn.Ping)
+- [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression
+- [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm)
 
 ## Roadmap
 
@@ -34,11 +34,7 @@ go get nhooyr.io/websocket
 
 ## Examples
 
-For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go).
-
-Use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError).
-There is also [websocket.CloseStatus](https://godoc.org/nhooyr.io/websocket#CloseStatus) to quickly grab the close status code out of a [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError).
-See the [CloseStatus godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseStatus).
+For a production quality example that demonstrates the full API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo).
 
 ### Server
 
@@ -87,83 +83,45 @@ c.Close(websocket.StatusNormalClosure, "")
 
 ## Comparison
 
-Before the comparison, I want to point out that gorilla/websocket was extremely useful in implementing the
-WebSocket protocol correctly so _big thanks_ to its authors. In particular, I made sure to go through the
-issue tracker of gorilla/websocket to ensure I implemented details correctly and understood how people were
-using WebSockets in production.
-
-### gorilla/websocket
-
-https://github.com/gorilla/websocket
-
-The implementation of gorilla/websocket is 6 years old. As such, it is
-widely used and very mature compared to nhooyr.io/websocket.
-
-On the other hand, it has grown organically and now there are too many ways to do
-the same thing. Compare the godoc of
-[nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) with
-[gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side.
-
-The API for nhooyr.io/websocket has been designed such that there is only one way to do things.
-This makes it easy to use correctly. Not only is the API simpler, the implementation is
-only 2200 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain,
-more code to test, more code to document and more surface area for bugs.
-
-Moreover, nhooyr.io/websocket supports newer Go idioms such as context.Context.
-It also uses net/http's Client and ResponseWriter directly for WebSocket handshakes.
-gorilla/websocket writes its handshakes to the underlying net.Conn.
-Thus it has to reinvent hooks for TLS and proxies and prevents easy support of HTTP/2.
-
-Some more advantages of nhooyr.io/websocket are that it supports concurrent writes and
-makes it very easy to close the connection with a status code and reason. In fact,
-nhooyr.io/websocket even implements the complete WebSocket close handshake for you whereas
-with gorilla/websocket you have to perform it manually. See [gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448).
-
-The ping API is also nicer. gorilla/websocket requires registering a pong handler on the Conn
-which results in awkward control flow. With nhooyr.io/websocket you use the Ping method on the Conn
-that sends a ping and also waits for the pong.
-
-Additionally, nhooyr.io/websocket can compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) for the browser.
-
-In terms of performance, the differences mostly depend on your application code. nhooyr.io/websocket
-reuses message buffers out of the box if you use the wsjson and wspb subpackages.
-As mentioned above, nhooyr.io/websocket also supports concurrent writers.
-
-The WebSocket masking algorithm used by this package is [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4)
-faster than gorilla/websocket while using only pure safe Go.
-
-The [permessage-deflate compression extension](https://tools.ietf.org/html/rfc7692) is fully supported by this library
-whereas gorilla only supports no context takeover mode. See our godoc for the differences. This will make a big
-difference on bandwidth used in most use cases.
-
-The only performance con to nhooyr.io/websocket is that it uses a goroutine to support
-cancellation with context.Context. This costs 2 KB of memory which is cheap compared to
-the benefits.
-
-### x/net/websocket
-
-https://godoc.org/golang.org/x/net/websocket
-
-Unmaintained and the API does not reflect WebSocket semantics. Should never be used.
-
-See https://github.com/golang/go/issues/18152
-
-### gobwas/ws
-
-https://github.com/gobwas/ws
-
-This library has an extremely flexible API but that comes at the cost of usability
-and clarity.
-
-Due to its flexibility, it can be used in a event driven style for performance.
-Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) about performant WebSocket servers.
-
-If you want a library that gives you absolute control over everything, this is the library.
-But for 99.9% of use cases, nhooyr.io/websocket will fit better as it is both easier and
-faster for normal idiomatic Go. The masking implementation is [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4)
-faster, the compression extensions are fully supported and as much as possible is reused by default.
-
-See the gorilla/websocket comparison for more performance details.
+### [gorilla/websocket](https://github.com/gorilla/websocket)
+
+Advantages of nhooyr.io/websocket:
+  - Minimal and idiomatic API
+    - Compare godoc of [nhooyr.io/websocket](https://godoc.org/nhooyr.io/websocket) with [gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side.
+  - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper
+  - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535))
+  - Full [context.Context](https://blog.golang.org/context) support
+  - Uses [net/http.Client](https://golang.org/pkg/net/http/#Client) for dialing
+    - Will enable easy HTTP/2 support in the future
+    - Gorilla writes directly to a net.Conn and so duplicates features from net/http.Client.
+  - Concurrent writes
+  - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448))
+  - Idiomatic [ping](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API
+    - gorilla/websocket requires registering a pong callback and then sending a Ping
+  - Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432))
+  - Transparent buffer reuse with [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages
+  - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go
+    - Gorilla's implementation depends on unsafe and is slower
+  - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support
+    - Gorilla only supports no context takeover mode
+  - [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper
+  - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370))
+
+Advantages of gorilla/websocket:
+  - Widely used and mature
+
+### [x/net/websocket](https://godoc.org/golang.org/x/net/websocket)
+
+Deprecated. See ([golang/go/issues/18152](https://github.com/golang/go/issues/18152)).
+
+The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper will ease in transitioning to nhooyr.io/websocket.
+
+### [gobwas/ws](https://github.com/gobwas/ws)
+
+This library has an extremely flexible API that allows it to be used in an unidiomatic event driven style
+for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). 
+
+When writing idiomatic Go, nhooyr.io/websocket is a better choice as it will be faster and easier to use.
 
 ## Users
 
diff --git a/close.go b/close.go
index baa1a7e07f0dc11d327c6f1ab2a12d6d761f4215..a02dc7d9e108cce3242d0814add8a9a628b519a1 100644
--- a/close.go
+++ b/close.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"log"
+	"nhooyr.io/websocket/internal/errd"
 	"time"
 
 	"nhooyr.io/websocket/internal/bpool"
@@ -96,15 +97,13 @@ func CloseStatus(err error) StatusCode {
 // Close will unblock all goroutines interacting with the connection once
 // complete.
 func (c *Conn) Close(code StatusCode, reason string) error {
-	err := c.closeHandshake(code, reason)
-	if err != nil {
-		return fmt.Errorf("failed to close WebSocket: %w", err)
-	}
-	return nil
+	return c.closeHandshake(code, reason)
 }
 
-func (c *Conn) closeHandshake(code StatusCode, reason string) error {
-	err := c.cw.sendClose(code, reason)
+func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) {
+	defer errd.Wrap(&err, "failed to close WebSocket")
+
+	err = c.cw.sendClose(code, reason)
 	if err != nil {
 		return err
 	}
@@ -115,7 +114,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) error {
 func (cw *connWriter) error(code StatusCode, err error) {
 	cw.c.setCloseErr(err)
 	cw.sendClose(code, err.Error())
-	cw.c.close(nil)
+	cw.c.closeWithErr(nil)
 }
 
 func (cw *connWriter) sendClose(code StatusCode, reason string) error {
@@ -135,7 +134,7 @@ func (cw *connWriter) sendClose(code StatusCode, reason string) error {
 }
 
 func (cr *connReader) waitClose() error {
-	defer cr.c.close(nil)
+	defer cr.c.closeWithErr(nil)
 
 	return nil
 
diff --git a/conn.go b/conn.go
index 5c041b8dc28324ab30e2449165a20cfc04ddc8ce..d9001791103690fed67f2ad9f702f73395bed449 100644
--- a/conn.go
+++ b/conn.go
@@ -33,11 +33,10 @@ const (
 // frames will not be handled. See the docs on Reader and CloseRead.
 //
 // Be sure to call Close on the connection when you
-// are finished with it to release the associated resources.
+// are finished with it to release associated resources.
 //
-// Every error from Read or Reader will cause the connection
-// to be closed so you do not need to write your own error message.
-// This applies to the Read methods in the wsjson/wspb subpackages as well.
+// On any error from any method, the connection is closed
+// with an appropriate reason.
 type Conn struct {
 	subprotocol string
 	rwc         io.ReadWriteCloser
@@ -69,11 +68,12 @@ type connConfig struct {
 }
 
 func newConn(cfg connConfig) *Conn {
-	c := &Conn{}
-	c.subprotocol = cfg.subprotocol
-	c.rwc = cfg.rwc
-	c.client = cfg.client
-	c.copts = cfg.copts
+	c := &Conn{
+		subprotocol: cfg.subprotocol,
+		rwc:         cfg.rwc,
+		client:      cfg.client,
+		copts:       cfg.copts,
+	}
 
 	c.cr.init(c, cfg.br)
 	c.cw.init(c, cfg.bw)
@@ -82,7 +82,7 @@ func newConn(cfg connConfig) *Conn {
 	c.activePings = make(map[string]chan<- struct{})
 
 	runtime.SetFinalizer(c, func(c *Conn) {
-		c.close(errors.New("connection garbage collected"))
+		c.closeWithErr(errors.New("connection garbage collected"))
 	})
 
 	go c.timeoutLoop()
@@ -96,7 +96,7 @@ func (c *Conn) Subprotocol() string {
 	return c.subprotocol
 }
 
-func (c *Conn) close(err error) {
+func (c *Conn) closeWithErr(err error) {
 	c.closeMu.Lock()
 	defer c.closeMu.Unlock()
 
@@ -135,7 +135,7 @@ func (c *Conn) timeoutLoop() {
 			c.cw.error(StatusPolicyViolation, errors.New("timed out"))
 			return
 		case <-writeCtx.Done():
-			c.close(fmt.Errorf("write timed out: %w", writeCtx.Err()))
+			c.closeWithErr(fmt.Errorf("write timed out: %w", writeCtx.Err()))
 			return
 		}
 	}
@@ -185,7 +185,7 @@ func (c *Conn) ping(ctx context.Context, p string) error {
 		return c.closeErr
 	case <-ctx.Done():
 		err := fmt.Errorf("failed to wait for pong: %w", ctx.Err())
-		c.close(err)
+		c.closeWithErr(err)
 		return err
 	case <-pong:
 		return nil
diff --git a/read.go b/read.go
index 13c8d703b4b2fbd2ba6ac6c4e74e5ee2f04779ce..7dba832a8c511109393188dc53c22652e63223d9 100644
--- a/read.go
+++ b/read.go
@@ -199,7 +199,7 @@ func (cr *connReader) frameHeader(ctx context.Context) (header, error) {
 		case <-ctx.Done():
 			return header{}, ctx.Err()
 		default:
-			cr.c.close(err)
+			cr.c.closeWithErr(err)
 			return header{}, err
 		}
 	}
@@ -229,7 +229,7 @@ func (cr *connReader) framePayload(ctx context.Context, p []byte) (int, error) {
 			return n, ctx.Err()
 		default:
 			err = fmt.Errorf("failed to read frame payload: %w", err)
-			cr.c.close(err)
+			cr.c.closeWithErr(err)
 			return n, err
 		}
 	}