Repository: creachadair/jrpc2
Pull request #97 (2023-03-19 17:09:37)

Collapse the code package into jrpc2.
After removing the registration hooks in #91, what is left of the code package is too small to deserve its own identity. Collapse the package into the root jrpc2 package.

- Rename the code.FromError helper to jrpc2.ErrorCode.
- Update documentation.

Updates #46.


Patch:
diff --git a/README.md b/README.md
index 9e05651..cd6fe9c 100644
--- a/README.md
+++ b/README.md
@@ -7,12 +7,10 @@ There is also a working [example in the Go playground](https://go.dev/play/p/Gdd
 
 ## Packages
 
-*  Package [jrpc2](http://godoc.org/github.com/creachadair/jrpc2) implements the base client and server.
+*  Package [jrpc2](http://godoc.org/github.com/creachadair/jrpc2) implements the base client and server and standard error codes.
 
 *  Package [channel](http://godoc.org/github.com/creachadair/jrpc2/channel) defines the communication channel abstraction used by the server & client.
 
-*  Package [code](http://godoc.org/github.com/creachadair/jrpc2/code) defines standard error codes as defined by the JSON-RPC 2.0 protocol.
-
 *  Package [handler](http://godoc.org/github.com/creachadair/jrpc2/handler) defines support for adapting functions to service methods.
 
 *  Package [jhttp](http://godoc.org/github.com/creachadair/jrpc2/jhttp) allows clients and servers to use HTTP as a transport.
diff --git a/base.go b/base.go
index 6cb54f5..3667dce 100644
--- a/base.go
+++ b/base.go
@@ -8,8 +8,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"strings"
-
-	"github.com/creachadair/jrpc2/code"
 )
 
 // An Assigner assigns a Handler to handle the specified method name, or nil if
@@ -236,9 +234,9 @@ func isServiceName(s string) bool {
 // error types. If err is not a context error, it is returned unchanged.
 func filterError(e *Error) error {
 	switch e.Code {
-	case code.Cancelled:
+	case Cancelled:
 		return context.Canceled
-	case code.DeadlineExceeded:
+	case DeadlineExceeded:
 		return context.DeadlineExceeded
 	}
 	return e
diff --git a/client.go b/client.go
index 804201d..8ff8a26 100644
--- a/client.go
+++ b/client.go
@@ -11,7 +11,6 @@ import (
 	"sync"
 
 	"github.com/creachadair/jrpc2/channel"
-	"github.com/creachadair/jrpc2/code"
 )
 
 // A Client is a JSON-RPC 2.0 client. The client sends requests and receives
@@ -210,7 +209,7 @@ func (c *Client) send(ctx context.Context, reqs jmessages) ([]*Response, error)
 	// on a closing path.
 	b, err := reqs.toJSON()
 	if err != nil {
-		return nil, Errorf(code.InternalError, "marshaling request failed: %v", err)
+		return nil, Errorf(InternalError, "marshaling request failed: %v", err)
 	}
 
 	var pends []*Response
@@ -266,9 +265,9 @@ func (c *Client) waitComplete(pctx context.Context, id string, p *Response) {
 
 	var jerr *Error
 	if c.err != nil && !isUninteresting(c.err) {
-		jerr = &Error{Code: code.InternalError, Message: c.err.Error()}
+		jerr = &Error{Code: InternalError, Message: c.err.Error()}
 	} else if err != nil {
-		jerr = &Error{Code: code.FromError(err), Message: err.Error()}
+		jerr = &Error{Code: ErrorCode(err), Message: err.Error()}
 	}
 
 	p.ch <- &jmessage{
@@ -427,7 +426,7 @@ func (c *Client) marshalParams(ctx context.Context, method string, params any) (
 	if fb := firstByte(pbits); fb != '[' && fb != '{' && !isNull(pbits) {
 		// JSON-RPC requires that if parameters are provided at all, they are
 		// an array or an object.
-		return nil, &Error{Code: code.InvalidRequest, Message: "invalid parameters: array or object required"}
+		return nil, &Error{Code: InvalidRequest, Message: "invalid parameters: array or object required"}
 	}
 	return pbits, nil
 }
diff --git a/code/code.go b/code.go
similarity index 85%
rename from code/code.go
rename to code.go
index 4a2ad0c..955ba39 100644
--- a/code/code.go
+++ b/code.go
@@ -1,7 +1,7 @@
 // Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved.
 
 // Package code defines error code values used by the jrpc2 package.
-package code
+package jrpc2
 
 import (
 	"context"
@@ -9,7 +9,7 @@ import (
 	"fmt"
 )
 
-// A Code is an error response code.
+// A Code is an error response.
 //
 // Code values from and including -32768 to -32000 are reserved for pre-defined
 // JSON-RPC errors.  Any code within this range, but not defined explicitly
@@ -49,9 +49,9 @@ func (c codeError) Is(err error) bool {
 	return ok && v.ErrCode() == Code(c)
 }
 
-// Err converts c to an error value, which is nil for code.NoError and
-// otherwise an error value whose code is c and whose text is based on the
-// built-in string for c if one exists.
+// Err converts c to an error value, which is nil for NoError and otherwise an
+// error value whose code is c and whose text is based on the built-in string
+// for c if one exists.
 func (c Code) Err() error {
 	if c == NoError {
 		return nil
@@ -69,7 +69,7 @@ const (
 	InvalidParams  Code = -32602 // [std] Invalid method parameters
 	InternalError  Code = -32603 // [std] Internal JSON-RPC error
 
-	NoError          Code = -32099 // Denotes a nil error (used by FromError)
+	NoError          Code = -32099 // Denotes a nil error (used by ErrorCode)
 	SystemError      Code = -32098 // Errors from the operating environment
 	Cancelled        Code = -32097 // Request cancelled (context.Canceled)
 	DeadlineExceeded Code = -32096 // Request deadline exceeded (context.DeadlineExceeded)
@@ -88,13 +88,13 @@ var stdError = map[Code]string{
 	DeadlineExceeded: "deadline exceeded",
 }
 
-// FromError returns a Code to categorize the specified error.
-// If err == nil, it returns code.NoError.
+// ErrorCode returns a Code to categorize the specified error.
+// If err == nil, it returns jrpc2.NoError.
 // If err is (or wraps) an ErrCoder, it returns the reported code value.
-// If err is context.Canceled, it returns code.Cancelled.
-// If err is context.DeadlineExceeded, it returns code.DeadlineExceeded.
-// Otherwise it returns code.SystemError.
-func FromError(err error) Code {
+// If err is context.Canceled, it returns jrpc2.Cancelled.
+// If err is context.DeadlineExceeded, it returns jrpc2.DeadlineExceeded.
+// Otherwise it returns jrpc2.SystemError.
+func ErrorCode(err error) Code {
 	if err == nil {
 		return NoError
 	}
diff --git a/error.go b/error.go
index 812122c..1eac43a 100644
--- a/error.go
+++ b/error.go
@@ -6,13 +6,11 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-
-	"github.com/creachadair/jrpc2/code"
 )
 
 // Error is the concrete type of errors returned from RPC calls.
 type Error struct {
-	Code    code.Code       `json:"code"`              // the machine-readable error code
+	Code    Code            `json:"code"`              // the machine-readable error code
 	Message string          `json:"message,omitempty"` // the human-readable error message
 	Data    json.RawMessage `json:"data,omitempty"`    // optional ancillary error data
 }
@@ -20,8 +18,8 @@ type Error struct {
 // Error renders e to a human-readable string for the error interface.
 func (e Error) Error() string { return fmt.Sprintf("[%d] %s", e.Code, e.Message) }
 
-// ErrCode trivially satisfies the code.ErrCoder interface for an *Error.
-func (e Error) ErrCode() code.Code { return e.Code }
+// ErrCode trivially satisfies the ErrCoder interface for an *Error.
+func (e Error) ErrCode() Code { return e.Code }
 
 // WithData marshals v as JSON and constructs a copy of e whose Data field
 // includes the result. If v == nil or if marshaling v fails, e is returned
@@ -44,22 +42,22 @@ var errServerStopped = errors.New("the server has been stopped")
 var errClientStopped = errors.New("the client has been stopped")
 
 // errEmptyMethod is the error reported for an empty request method name.
-var errEmptyMethod = &Error{Code: code.InvalidRequest, Message: "empty method name"}
+var errEmptyMethod = &Error{Code: InvalidRequest, Message: "empty method name"}
 
 // errNoSuchMethod is the error reported for an unknown method name.
-var errNoSuchMethod = &Error{Code: code.MethodNotFound, Message: code.MethodNotFound.String()}
+var errNoSuchMethod = &Error{Code: MethodNotFound, Message: MethodNotFound.String()}
 
 // errDuplicateID is the error reported for a duplicated request ID.
-var errDuplicateID = &Error{Code: code.InvalidRequest, Message: "duplicate request ID"}
+var errDuplicateID = &Error{Code: InvalidRequest, Message: "duplicate request ID"}
 
 // errInvalidRequest is the error reported for an invalid request object or batch.
-var errInvalidRequest = &Error{Code: code.ParseError, Message: "invalid request value"}
+var errInvalidRequest = &Error{Code: ParseError, Message: "invalid request value"}
 
 // errEmptyBatch is the error reported for an empty request batch.
-var errEmptyBatch = &Error{Code: code.InvalidRequest, Message: "empty request batch"}
+var errEmptyBatch = &Error{Code: InvalidRequest, Message: "empty request batch"}
 
 // errInvalidParams is the error reported for invalid request parameters.
-var errInvalidParams = &Error{Code: code.InvalidParams, Message: code.InvalidParams.String()}
+var errInvalidParams = &Error{Code: InvalidParams, Message: InvalidParams.String()}
 
 // errTaskNotExecuted is the internal sentinel error for an unassigned task.
 var errTaskNotExecuted = new(Error)
@@ -70,6 +68,6 @@ var ErrConnClosed = errors.New("client connection is closed")
 
 // Errorf returns an error value of concrete type *Error having the specified
 // code and formatted message string.
-func Errorf(code code.Code, msg string, args ...any) *Error {
+func Errorf(code Code, msg string, args ...any) *Error {
 	return &Error{Code: code, Message: fmt.Sprintf(msg, args...)}
 }
diff --git a/handler/handler.go b/handler/handler.go
index 1fdbf8a..e17540c 100644
--- a/handler/handler.go
+++ b/handler/handler.go
@@ -14,7 +14,6 @@ import (
 	"strings"
 
 	"github.com/creachadair/jrpc2"
-	"github.com/creachadair/jrpc2/code"
 )
 
 // A Func is wrapper for a jrpc2.Handler function.
@@ -96,7 +95,7 @@ var (
 
 	strictType = reflect.TypeOf((*interface{ DisallowUnknownFields() })(nil)).Elem()
 
-	errNoParameters = &jrpc2.Error{Code: code.InvalidParams, Message: "no parameters accepted"}
+	errNoParameters = &jrpc2.Error{Code: jrpc2.InvalidParams, Message: "no parameters accepted"}
 )
 
 // FuncInfo captures type signature information from a valid handler function.
@@ -179,7 +178,7 @@ func (fi *FuncInfo) Wrap() Func {
 		newInput = func(ctx reflect.Value, req *jrpc2.Request) ([]reflect.Value, error) {
 			in := reflect.New(arg.Elem())
 			if err := req.UnmarshalParams(wrapArg(in)); err != nil {
-				return nil, jrpc2.Errorf(code.InvalidParams, "invalid parameters: %v", err)
+				return nil, jrpc2.Errorf(jrpc2.InvalidParams, "invalid parameters: %v", err)
 			}
 			return []reflect.Value{ctx, in}, nil
 		}
@@ -188,7 +187,7 @@ func (fi *FuncInfo) Wrap() Func {
 		newInput = func(ctx reflect.Value, req *jrpc2.Request) ([]reflect.Value, error) {
 			in := reflect.New(arg) // we still need a pointer to unmarshal
 			if err := req.UnmarshalParams(wrapArg(in)); err != nil {
-				return nil, jrpc2.Errorf(code.InvalidParams, "invalid parameters: %v", err)
+				return nil, jrpc2.Errorf(jrpc2.InvalidParams, "invalid parameters: %v", err)
 			}
 			// Indirect the pointer back off for the callee.
 			return []reflect.Value{ctx, in.Elem()}, nil
@@ -333,7 +332,7 @@ func (s *arrayStub) translate(data []byte) ([]byte, error) {
 	if err := json.Unmarshal(data, &arr); err != nil {
 		return nil, err
 	} else if len(arr) != len(s.posNames) {
-		return nil, jrpc2.Errorf(code.InvalidParams, "got %d parameters, want %d",
+		return nil, jrpc2.Errorf(jrpc2.InvalidParams, "got %d parameters, want %d",
 			len(arr), len(s.posNames))
 	}
 
diff --git a/jhttp/getter.go b/jhttp/getter.go
index 154dfee..58d59c9 100644
--- a/jhttp/getter.go
+++ b/jhttp/getter.go
@@ -12,7 +12,6 @@ import (
 	"strings"
 
 	"github.com/creachadair/jrpc2"
-	"github.com/creachadair/jrpc2/code"
 	"github.com/creachadair/jrpc2/server"
 )
 
@@ -67,7 +66,7 @@ func (g Getter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	method, params, err := g.parseHTTPRequest(req)
 	if err != nil {
 		writeJSON(w, http.StatusBadRequest, &jrpc2.Error{
-			Code:    code.ParseError,
+			Code:    jrpc2.ParseError,
 			Message: err.Error(),
 		})
 		return
@@ -76,8 +75,8 @@ func (g Getter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	var result json.RawMessage
 	if err := g.local.Client.CallResult(req.Context(), method, params, &result); err != nil {
 		var status int
-		switch code.FromError(err) {
-		case code.MethodNotFound:
+		switch jrpc2.ErrorCode(err) {
+		case jrpc2.MethodNotFound:
 			status = http.StatusNotFound
 		default:
 			status = http.StatusInternalServerError
diff --git a/json.go b/json.go
index 1dbb8b7..e04ed59 100644
--- a/json.go
+++ b/json.go
@@ -5,8 +5,6 @@ package jrpc2
 import (
 	"bytes"
 	"encoding/json"
-
-	"github.com/creachadair/jrpc2/code"
 )
 
 // ParseRequests parses a single request or a batch of requests from JSON.
@@ -151,7 +149,7 @@ func isValidID(v json.RawMessage) bool {
 // isValidVersion reports whether v is a valid JSON-RPC version string.
 func isValidVersion(v string) bool { return v == Version }
 
-func (j *jmessage) fail(code code.Code, msg string) {
+func (j *jmessage) fail(code Code, msg string) {
 	if j.err == nil {
 		j.err = &Error{Code: code, Message: msg}
 	}
@@ -203,7 +201,7 @@ func (j *jmessage) parseJSON(data []byte) error {
 
 	var obj map[string]json.RawMessage
 	if err := json.Unmarshal(data, &obj); err != nil {
-		j.fail(code.ParseError, "request is not a JSON object")
+		j.fail(ParseError, "request is not a JSON object")
 		return j.err
 	}
 
@@ -213,17 +211,17 @@ func (j *jmessage) parseJSON(data []byte) error {
 		switch key {
 		case "jsonrpc":
 			if json.Unmarshal(val, &j.V) != nil {
-				j.fail(code.ParseError, "invalid version key")
+				j.fail(ParseError, "invalid version key")
 			}
 		case "id":
 			if isValidID(val) {
 				j.ID = val
 			} else {
-				j.fail(code.InvalidRequest, "invalid request ID")
+				j.fail(InvalidRequest, "invalid request ID")
 			}
 		case "method":
 			if json.Unmarshal(val, &j.M) != nil {
-				j.fail(code.ParseError, "invalid method name")
+				j.fail(ParseError, "invalid method name")
 			}
 		case "params":
 			// As a special case, reduce "null" to nil in the parameters.
@@ -232,11 +230,11 @@ func (j *jmessage) parseJSON(data []byte) error {
 				j.P = val
 			}
 			if fb := firstByte(j.P); fb != 0 && fb != '[' && fb != '{' {
-				j.fail(code.InvalidRequest, "parameters must be array or object")
+				j.fail(InvalidRequest, "parameters must be array or object")
 			}
 		case "error":
 			if json.Unmarshal(val, &j.E) != nil {
-				j.fail(code.ParseError, "invalid error value")
+				j.fail(ParseError, "invalid error value")
 			}
 		case "result":
 			j.R = val
@@ -247,17 +245,17 @@ func (j *jmessage) parseJSON(data []byte) error {
 
 	// Report an error for an invalid version marker
 	if !isValidVersion(j.V) {
-		j.fail(code.InvalidRequest, "invalid version marker")
+		j.fail(InvalidRequest, "invalid version marker")
 	}
 
 	// Report an error if request/response fields overlap.
 	if j.M != "" && (j.E != nil || j.R != nil) {
-		j.fail(code.InvalidRequest, "mixed request and reply fields")
+		j.fail(InvalidRequest, "mixed request and reply fields")
 	}
 
 	// Report an error for extraneous fields.
 	if j.err == nil && len(extra) != 0 {
-		j.err = Errorf(code.InvalidRequest, "extra fields in request").WithData(extra)
+		j.err = Errorf(InvalidRequest, "extra fields in request").WithData(extra)
 	}
 	return nil
 }
diff --git a/opts.go b/opts.go
index ce7f593..6b29cb7 100644
--- a/opts.go
+++ b/opts.go
@@ -9,8 +9,6 @@ import (
 	"log"
 	"runtime"
 	"time"
-
-	"github.com/creachadair/jrpc2/code"
 )
 
 // ServerOptions control the behaviour of a server created by NewServer.
@@ -177,7 +175,7 @@ func (c *ClientOptions) handleCallback() func(context.Context, *jmessage) []byte
 			if e, ok := err.(*Error); ok {
 				rsp.E = e
 			} else {
-				rsp.E = &Error{Code: code.FromError(err), Message: err.Error()}
+				rsp.E = &Error{Code: ErrorCode(err), Message: err.Error()}
 			}
 		}
 		bits, _ := rsp.toJSON()
diff --git a/server.go b/server.go
index 55902aa..1a15b01 100644
--- a/server.go
+++ b/server.go
@@ -14,7 +14,6 @@ import (
 	"time"
 
 	"github.com/creachadair/jrpc2/channel"
-	"github.com/creachadair/jrpc2/code"
 	"github.com/creachadair/mds/mlink"
 	"golang.org/x/sync/semaphore"
 )
@@ -475,7 +474,7 @@ func (s *Server) waitCallback(pctx context.Context, id string, p *Response) {
 
 	p.ch <- &jmessage{
 		ID: json.RawMessage(id),
-		E:  &Error{Code: code.FromError(err), Message: err.Error()},
+		E:  &Error{Code: ErrorCode(err), Message: err.Error()},
 	}
 }
 
@@ -737,7 +736,7 @@ func (s *Server) pushError(err error) {
 	if e, ok := err.(*Error); ok {
 		jerr = e
 	} else {
-		jerr = &Error{Code: code.FromError(err), Message: err.Error()}
+		jerr = &Error{Code: ErrorCode(err), Message: err.Error()}
 	}
 
 	nw, err := encode(s.ch, jmessages{{
@@ -789,7 +788,7 @@ func (ts tasks) responses(rpcLog RPCLogger) jmessages {
 			//
 			// However, parse and validation errors must still be reported, with
 			// an ID of null if the request ID was not resolvable.
-			if c := code.FromError(task.err); c != code.ParseError && c != code.InvalidRequest {
+			if c := ErrorCode(task.err); c != ParseError && c != InvalidRequest {
 				continue
 			}
 		}
@@ -805,10 +804,10 @@ func (ts tasks) responses(rpcLog RPCLogger) jmessages {
 			rsp.R = task.val
 		} else if e, ok := task.err.(*Error); ok {
 			rsp.E = e
-		} else if c := code.FromError(task.err); c != code.NoError {
+		} else if c := ErrorCode(task.err); c != NoError {
 			rsp.E = &Error{Code: c, Message: task.err.Error()}
 		} else {
-			rsp.E = &Error{Code: code.InternalError, Message: task.err.Error()}
+			rsp.E = &Error{Code: InternalError, Message: task.err.Error()}
 		}
 		rpcLog.LogResponse(task.ctx, &Response{
 			id:     string(rsp.ID),


Test patch:
diff --git a/code/code_test.go b/code_test.go
similarity index 54%
rename from code/code_test.go
rename to code_test.go
index 1b3c202..f35f8e7 100644
--- a/code/code_test.go
+++ b/code_test.go
@@ -1,6 +1,6 @@
 // Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved.
 
-package code_test
+package jrpc2_test
 
 import (
 	"context"
@@ -9,49 +9,49 @@ import (
 	"io"
 	"testing"
 
-	"github.com/creachadair/jrpc2/code"
+	"github.com/creachadair/jrpc2"
 )
 
-type testCoder code.Code
+type testCoder jrpc2.Code
 
-func (t testCoder) ErrCode() code.Code { return code.Code(t) }
-func (testCoder) Error() string        { return "bogus" }
+func (t testCoder) ErrCode() jrpc2.Code { return jrpc2.Code(t) }
+func (testCoder) Error() string         { return "bogus" }
 
-func TestFromError(t *testing.T) {
+func TestErrorCode(t *testing.T) {
 	tests := []struct {
 		input error
-		want  code.Code
+		want  jrpc2.Code
 	}{
-		{nil, code.NoError},
-		{testCoder(code.ParseError), code.ParseError},
-		{testCoder(code.InvalidRequest), code.InvalidRequest},
-		{fmt.Errorf("wrapped parse error: %w", code.ParseError.Err()), code.ParseError},
-		{context.Canceled, code.Cancelled},
-		{fmt.Errorf("wrapped cancellation: %w", context.Canceled), code.Cancelled},
-		{context.DeadlineExceeded, code.DeadlineExceeded},
-		{fmt.Errorf("wrapped deadline: %w", context.DeadlineExceeded), code.DeadlineExceeded},
-		{errors.New("other"), code.SystemError},
-		{io.EOF, code.SystemError},
+		{nil, jrpc2.NoError},
+		{testCoder(jrpc2.ParseError), jrpc2.ParseError},
+		{testCoder(jrpc2.InvalidRequest), jrpc2.InvalidRequest},
+		{fmt.Errorf("wrapped parse error: %w", jrpc2.ParseError.Err()), jrpc2.ParseError},
+		{context.Canceled, jrpc2.Cancelled},
+		{fmt.Errorf("wrapped cancellation: %w", context.Canceled), jrpc2.Cancelled},
+		{context.DeadlineExceeded, jrpc2.DeadlineExceeded},
+		{fmt.Errorf("wrapped deadline: %w", context.DeadlineExceeded), jrpc2.DeadlineExceeded},
+		{errors.New("other"), jrpc2.SystemError},
+		{io.EOF, jrpc2.SystemError},
 	}
 	for _, test := range tests {
-		if got := code.FromError(test.input); got != test.want {
-			t.Errorf("FromError(%v): got %v, want %v", test.input, got, test.want)
+		if got := jrpc2.ErrorCode(test.input); got != test.want {
+			t.Errorf("ErrorCode(%v): got %v, want %v", test.input, got, test.want)
 		}
 	}
 }
 
 func TestCodeIs(t *testing.T) {
 	tests := []struct {
-		code code.Code
+		code jrpc2.Code
 		err  error
 		want bool
 	}{
-		{code.NoError, nil, true},
+		{jrpc2.NoError, nil, true},
 		{0, nil, false},
-		{1, code.Code(1).Err(), true},
-		{2, code.Code(3).Err(), false},
-		{4, fmt.Errorf("blah: %w", code.Code(4).Err()), true},
-		{5, fmt.Errorf("nope: %w", code.Code(6).Err()), false},
+		{1, jrpc2.Code(1).Err(), true},
+		{2, jrpc2.Code(3).Err(), false},
+		{4, fmt.Errorf("blah: %w", jrpc2.Code(4).Err()), true},
+		{5, fmt.Errorf("nope: %w", jrpc2.Code(6).Err()), false},
 	}
 	for _, test := range tests {
 		cerr := test.code.Err()
@@ -67,11 +67,11 @@ func TestErr(t *testing.T) {
 		return e1 == e2 || (e1 != nil && e2 != nil && e1.Error() == e2.Error())
 	}
 	type test struct {
-		code code.Code
+		code jrpc2.Code
 		want error
 	}
 	tests := []test{
-		{code.NoError, nil},
+		{jrpc2.NoError, nil},
 		{0, errors.New("error code 0")},
 		{1, errors.New("error code 1")},
 		{-17, errors.New("error code -17")},
@@ -84,7 +84,7 @@ func TestErr(t *testing.T) {
 		// Codes reserved by this implementation.
 		-32098, -32097, -32096,
 	} {
-		c := code.Code(v)
+		c := jrpc2.Code(v)
 		tests = append(tests, test{
 			code: c,
 			want: errors.New(c.String()),
@@ -95,7 +95,7 @@ func TestErr(t *testing.T) {
 		if !eqv(got, test.want) {
 			t.Errorf("Code(%d).Err(): got %#v, want %#v", test.code, got, test.want)
 		}
-		if c := code.FromError(got); c != test.code {
+		if c := jrpc2.ErrorCode(got); c != test.code {
 			t.Errorf("Code(%d).Err(): got code %v, want %v", test.code, c, test.code)
 		}
 	}
diff --git a/examples_test.go b/examples_test.go
index cd63232..af9be28 100644
--- a/examples_test.go
+++ b/examples_test.go
@@ -11,7 +11,6 @@ import (
 	"strings"
 
 	"github.com/creachadair/jrpc2"
-	"github.com/creachadair/jrpc2/code"
 	"github.com/creachadair/jrpc2/handler"
 	"github.com/creachadair/jrpc2/internal/testutil"
 	"github.com/creachadair/jrpc2/server"
@@ -119,14 +118,14 @@ func ExampleRequest_UnmarshalParams() {
 	//
 	// Solution 1: Use the jrpc2.StrictFields helper.
 	err = req.UnmarshalParams(jrpc2.StrictFields(&t))
-	if code.FromError(err) != code.InvalidParams {
+	if jrpc2.ErrorCode(err) != jrpc2.InvalidParams {
 		log.Fatalf("UnmarshalParams strict: %v", err)
 	}
 
 	// Solution 2: Implement a DisallowUnknownFields method.
 	var p strictParams
 	err = req.UnmarshalParams(&p)
-	if code.FromError(err) != code.InvalidParams {
+	if jrpc2.ErrorCode(err) != jrpc2.InvalidParams {
 		log.Fatalf("UnmarshalParams strict: %v", err)
 	}
 
diff --git a/handler/handler_test.go b/handler/handler_test.go
index 3b37de1..971f96e 100644
--- a/handler/handler_test.go
+++ b/handler/handler_test.go
@@ -11,7 +11,6 @@ import (
 	"testing"
 
 	"github.com/creachadair/jrpc2"
-	"github.com/creachadair/jrpc2/code"
 	"github.com/creachadair/jrpc2/handler"
 	"github.com/creachadair/jrpc2/internal/testutil"
 	"github.com/google/go-cmp/cmp"
@@ -254,7 +253,7 @@ func TestFuncInfo_SetStrict(t *testing.T) {
       "Z": 25
    }}`)
 	rsp, err := fn(context.Background(), req)
-	if got := code.FromError(err); got != code.InvalidParams {
+	if got := jrpc2.ErrorCode(err); got != jrpc2.InvalidParams {
 		t.Errorf("Handler returned (%+v, %v), want InvalidParms", rsp, err)
 	}
 }
diff --git a/internal_test.go b/internal_test.go
index a306182..ce31c85 100644
--- a/internal_test.go
+++ b/internal_test.go
@@ -13,13 +13,12 @@ import (
 	"time"
 
 	"github.com/creachadair/jrpc2/channel"
-	"github.com/creachadair/jrpc2/code"
 	"github.com/fortytw2/leaktest"
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 )
 
-var errInvalidVersion = &Error{Code: code.InvalidRequest, Message: "invalid version marker"}
+var errInvalidVersion = &Error{Code: InvalidRequest, Message: "invalid version marker"}
 
 func TestParseRequests(t *testing.T) {
 	tests := []struct {
@@ -62,10 +61,10 @@ func TestParseRequests(t *testing.T) {
 		}, nil},
 
 		// A broken request.
-		{`{`, nil, Errorf(code.ParseError, "invalid request value")},
+		{`{`, nil, Errorf(ParseError, "invalid request value")},
 
 		// A broken batch.
-		{`["bad"{]`, nil, Errorf(code.ParseError, "invalid request value")},
+		{`["bad"{]`, nil, Errorf(ParseError, "invalid request value")},
 	}
 	for _, test := range tests {
 		got, err := ParseRequests([]byte(test.input))
@@ -87,7 +86,7 @@ func errEQ(x, y error) bool {
 	} else if y == nil {
 		return false
 	}
-	return code.FromError(x) == code.FromError(y) && x.Error() == y.Error()
+	return ErrorCode(x) == ErrorCode(y) && x.Error() == y.Error()
 }
 
 func TestRequest_UnmarshalParams(t *testing.T) {
@@ -100,24 +99,24 @@ func TestRequest_UnmarshalParams(t *testing.T) {
 		input   string
 		want    any
 		pstring string
-		code    code.Code
+		code    Code
 	}{
 		// If parameters are set, the target should be updated.
-		{`{"jsonrpc":"2.0", "id":1, "method":"X", "params":[1,2]}`, []int{1, 2}, "[1,2]", code.NoError},
+		{`{"jsonrpc":"2.0", "id":1, "method":"X", "params":[1,2]}`, []int{1, 2}, "[1,2]", NoError},
 
 		// If parameters are null, the target should not be modified.
-		{`{"jsonrpc":"2.0", "id":2, "method":"Y", "params":null}`, "", "", code.NoError},
+		{`{"jsonrpc":"2.0", "id":2, "method":"Y", "params":null}`, "", "", NoError},
 
 		// If parameters are not set, the target should not be modified.
-		{`{"jsonrpc":"2.0", "id":2, "method":"Y"}`, 0, "", code.NoError},
+		{`{"jsonrpc":"2.0", "id":2, "method":"Y"}`, 0, "", NoError},
 
 		// Unmarshaling should work into a struct as long as the fields match.
-		{`{"jsonrpc":"2.0", "id":3, "method":"Z", "params":{}}`, xy{}, "{}", code.NoError},
-		{`{"jsonrpc":"2.0", "id":4, "method":"Z", "params":{"x":17}}`, xy{X: 17}, `{"x":17}`, code.NoError},
+		{`{"jsonrpc":"2.0", "id":3, "method":"Z", "params":{}}`, xy{}, "{}", NoError},
+		{`{"jsonrpc":"2.0", "id":4, "method":"Z", "params":{"x":17}}`, xy{X: 17}, `{"x":17}`, NoError},
 		{`{"jsonrpc":"2.0", "id":5, "method":"Z", "params":{"x":23, "y":true}}`,
-			xy{X: 23, Y: true}, `{"x":23, "y":true}`, code.NoError},
+			xy{X: 23, Y: true}, `{"x":23, "y":true}`, NoError},
 		{`{"jsonrpc":"2.0", "id":6, "method":"Z", "params":{"x":23, "z":"wat"}}`,
-			xy{X: 23}, `{"x":23, "z":"wat"}`, code.NoError},
+			xy{X: 23}, `{"x":23, "z":"wat"}`, NoError},
 	}
 	for _, test := range tests {
 		var reqs jmessages
@@ -132,7 +131,7 @@ func TestRequest_UnmarshalParams(t *testing.T) {
 		target := reflect.New(reflect.TypeOf(test.want)).Interface()
 		{
 			err := req.UnmarshalParams(target)
-			if got := code.FromError(err); got != test.code {
+			if got := ErrorCode(err); got != test.code {
 				t.Errorf("UnmarshalParams error: got code %d, want %d [%v]", got, test.code, err)
 			}
 			if err != nil {
@@ -207,8 +206,8 @@ func TestClient_contextCancellation(t *testing.T) {
 	rsp.wait()
 	close(stopped)
 	if err := rsp.Error(); err != nil {
-		if err.Code != code.Cancelled {
-			t.Errorf("Response error for %q: got %v, want %v", rsp.ID(), err, code.Cancelled)
+		if err.Code != Cancelled {
+			t.Errorf("Response error for %q: got %v, want %v", rsp.ID(), err, Cancelled)
 		}
 	} else {
 		t.Errorf("Response for %q: unexpectedly succeeded", rsp.ID())
@@ -310,7 +309,7 @@ func TestMarshalResponse(t *testing.T) {
 	}{
 		{"", nil, "", `{"jsonrpc":"2.0"}`},
 		{"null", nil, "", `{"jsonrpc":"2.0","id":null}`},
-		{"123", Errorf(code.ParseError, "failed"), "",
+		{"123", Errorf(ParseError, "failed"), "",
 			`{"jsonrpc":"2.0","id":123,"error":{"code":-32700,"message":"failed"}}`},
 		{"456", nil, `{"ok":true,"values":[4,5,6]}`,
 			`{"jsonrpc":"2.0","id":456,"result":{"ok":true,"values":[4,5,6]}}`},
diff --git a/jrpc2_test.go b/jrpc2_test.go
index 3c65355..6f4e574 100644
--- a/jrpc2_test.go
+++ b/jrpc2_test.go
@@ -16,7 +16,6 @@ import (
 
 	"github.com/creachadair/jrpc2"
 	"github.com/creachadair/jrpc2/channel"
-	"github.com/creachadair/jrpc2/code"
 	"github.com/creachadair/jrpc2/handler"
 	"github.com/creachadair/jrpc2/server"
 	"github.com/fortytw2/leaktest"
@@ -25,7 +24,7 @@ import (
 
 // Static type assertions.
 var (
-	_ code.ErrCoder = (*jrpc2.Error)(nil)
+	_ jrpc2.ErrCoder = (*jrpc2.Error)(nil)
 )
 
 var testOK = handler.New(func(ctx context.Context) (string, error) {
@@ -71,7 +70,7 @@ func (dummy) Mul(_ context.Context, req struct{ X, Y int }) (int, error) {
 // Max takes a slice of arguments.
 func (dummy) Max(_ context.Context, vs []int) (int, error) {
 	if len(vs) == 0 {
-		return 0, jrpc2.Errorf(code.InvalidParams, "cannot compute max of no elements")
+		return 0, jrpc2.Errorf(jrpc2.InvalidParams, "cannot compute max of no elements")
 	}
 	max := vs[0]
 	for _, v := range vs[1:] {
@@ -406,8 +405,8 @@ func TestServer_stopCancelsHandlers(t *testing.T) {
 	case <-time.After(30 * time.Second):
 		t.Error("Timed out waiting for service handler to fail")
 	case err := <-stopped:
-		if ec := code.FromError(err); ec != code.Cancelled {
-			t.Errorf("Client error: got %v (%v), wanted code %v", err, ec, code.Cancelled)
+		if ec := jrpc2.ErrorCode(err); ec != jrpc2.Cancelled {
+			t.Errorf("Client error: got %v (%v), wanted code %v", err, ec, jrpc2.Cancelled)
 		}
 	}
 }
@@ -456,9 +455,9 @@ func TestServer_CancelRequest(t *testing.T) {
 	}
 
 	err := <-errc
-	got := code.FromError(err)
-	if got != code.Cancelled {
-		t.Errorf("Stall: got %v (%v), want %v", err, got, code.Cancelled)
+	got := jrpc2.ErrorCode(err)
+	if got != jrpc2.Cancelled {
+		t.Errorf("Stall: got %v (%v), want %v", err, got, jrpc2.Cancelled)
 	} else {
 		t.Logf("Cancellation succeeded, got expected error: %v", err)
 	}
@@ -480,7 +479,7 @@ func TestError_withData(t *testing.T) {
 			return false, jrpc2.ServerFromContext(ctx).Notify(ctx, "PushBack", nil)
 		}),
 		"Code": handler.New(func(ctx context.Context) error {
-			return code.Code(12345).Err()
+			return jrpc2.Code(12345).Err()
 		}),
 	}, &server.LocalOptions{
 		Client: &jrpc2.ClientOptions{
@@ -533,7 +532,7 @@ func TestClient_badCallParams(t *testing.T) {
 	rsp, err := loc.Client.Call(context.Background(), "Test", "bogus")
 	if err == nil {
 		t.Errorf("Call(Test): got %+v, wanted error", rsp)
-	} else if got, want := code.FromError(err), code.InvalidRequest; got != want {
+	} else if got, want := jrpc2.ErrorCode(err), jrpc2.InvalidRequest; got != want {
 		t.Errorf("Call(Test): got code %v, want %v", got, want)
 	}
 }
@@ -869,7 +868,7 @@ func TestClient_onCancelHook(t *testing.T) {
 		"computerSaysNo": handler.New(func(ctx context.Context, ids []string) error {
 			defer close(hooked)
 			if req := jrpc2.InboundRequest(ctx); !req.IsNotification() {
-				return jrpc2.Errorf(code.MethodNotFound, "no such method %q", req.Method())
+				return jrpc2.Errorf(jrpc2.MethodNotFound, "no such method %q", req.Method())
 			}
 			srv := jrpc2.ServerFromContext(ctx)
 			for _, id := range ids {
@@ -1069,8 +1068,8 @@ func TestHandler_noParams(t *testing.T) {
 	var rsp string
 	if err := loc.Client.CallResult(context.Background(), "Test", []int{1, 2, 3}, &rsp); err == nil {
 		t.Errorf("Call(Test): got %q, wanted error", rsp)
-	} else if ec := code.FromError(err); ec != code.InvalidParams {
-		t.Errorf("Call(Test): got code %v, wanted %v", ec, code.InvalidParams)
+	} else if ec := jrpc2.ErrorCode(err); ec != jrpc2.InvalidParams {
+		t.Errorf("Call(Test): got code %v, wanted %v", ec, jrpc2.InvalidParams)
 	}
 }
 
@@ -1233,17 +1232,17 @@ func TestRequest_strictFields(t *testing.T) {
 	tests := []struct {
 		method string
 		params any
-		code   code.Code
+		code   jrpc2.Code
 		want   string
 	}{
-		{"Strict", handler.Obj{"alpha": "aiuto"}, code.NoError, "aiuto"},
-		{"Strict", handler.Obj{"alpha": "selva me", "charlie": true}, code.NoError, "selva me"},
-		{"Strict", handler.Obj{"alpha": "OK", "nonesuch": true}, code.InvalidParams, ""},
-		{"Normal", handler.Obj{"alpha": "OK", "nonesuch": true}, code.NoError, "OK"},
+		{"Strict", handler.Obj{"alpha": "aiuto"}, jrpc2.NoError, "aiuto"},
+		{"Strict", handler.Obj{"alpha": "selva me", "charlie": true}, jrpc2.NoError, "selva me"},
+		{"Strict", handler.Obj{"alpha": "OK", "nonesuch": true}, jrpc2.InvalidParams, ""},
+		{"Normal", handler.Obj{"alpha": "OK", "nonesuch": true}, jrpc2.NoError, "OK"},
 	}
 	for _, test := range tests {
 		name := test.method + "/"
-		if test.code == code.NoError {
+		if test.code == jrpc2.NoError {
 			name += "OK"
 		} else {
 			name += test.code.String()
@@ -1251,10 +1250,10 @@ func TestRequest_strictFields(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			var res string
 			err := loc.Client.CallResult(ctx, test.method, test.params, &res)
-			if err == nil && test.code != code.NoError {
+			if err == nil && test.code != jrpc2.NoError {
 				t.Errorf("CallResult: got %+v, want error code %v", res, test.code)
 			} else if err != nil {
-				if c := code.FromError(err); c != test.code {
+				if c := jrpc2.ErrorCode(err); c != test.code {
 					t.Errorf("CallResult: got error %v, wanted code %v", err, test.code)
 				}
 			} else if res != test.want {