Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document @solana/errors with TypeDoc #66

Merged
merged 1 commit into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors/#adding-a-new-error
*
* @module
* @privateRemarks
* WARNING:
* - Don't remove error codes
* - Don't change or reorder error codes.
Expand Down Expand Up @@ -302,6 +304,7 @@ export const SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEME
/**
* A union of every Solana error code
*
* @privateRemarks
* You might be wondering why this is not a TypeScript enum or const enum.
*
* One of the goals of this library is to enable people to use some or none of it without having to
Expand Down Expand Up @@ -541,6 +544,7 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_VOTE_COST_LIMIT;

/**
* Errors of this type are understood to have an optional `SolanaError` nested inside as `cause`.
* Errors of this type are understood to have an optional {@link SolanaError} nested inside as
* `cause`.
*/
export type SolanaErrorCodeWithCause = typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE;
14 changes: 9 additions & 5 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors/#adding-a-new-error
*
* @privateRemarks
* WARNING:
* - Don't change or remove members of an error's context.
*/
import {
SOLANA_ERROR__ACCOUNTS__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__ACCOUNTS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED,
Expand Down Expand Up @@ -167,11 +175,7 @@ interface ReadonlyUint8Array extends Omit<Uint8Array, TypedArrayMutablePropertie
}

/**
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors/#adding-a-new-error
*
* WARNING:
* - Don't change or remove members of an error's context.
* A map of every {@link SolanaError} code to the type of its `context` property.
*/
export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
BasicInstructionErrorContext<
Expand Down
59 changes: 59 additions & 0 deletions packages/errors/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,54 @@ import { SolanaErrorCode, SolanaErrorCodeWithCause } from './codes';
import { SolanaErrorContext } from './context';
import { getErrorMessage } from './message-formatter';

/**
* A type guard that returns `true` if the input is a {@link SolanaError}, optionally with a
* particular error code.
*
* When the `code` argument is supplied and the input is a {@link SolanaError}, TypeScript will
* refine the error's {@link SolanaError#context | `context`} property to the type associated with
* that error code. You can use that context to render useful error messages, or to make
* context-aware decisions that help your application to recover from the error.
Comment on lines +6 to +12
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully documenting this here will make it show in in people's autocomplete, then they'll see the example, and better understand how to take advantage of the full power of this assertion.

*
* @example
* ```ts
* import {
* SOLANA_ERROR__TRANSACTION__MISSING_SIGNATURE,
* SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING,
* isSolanaError,
* } from '@solana/errors';
* import { assertTransactionIsFullySigned, getSignatureFromTransaction } from '@solana/transactions';
*
* try {
* const transactionSignature = getSignatureFromTransaction(tx);
* assertTransactionIsFullySigned(tx);
* /* ... *\/
* } catch (e) {
* if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) {
* displayError(
* "We can't send this transaction without signatures for these addresses:\n- %s",
* // The type of the `context` object is now refined to contain `addresses`.
* e.context.addresses.join('\n- '),
* );
* return;
* } else if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING)) {
* if (!tx.feePayer) {
* displayError('Choose a fee payer for this transaction before sending it');
* } else {
* displayError('The fee payer still needs to sign for this transaction');
* }
* return;
* }
* throw e;
* }
* ```
*/
export function isSolanaError<TErrorCode extends SolanaErrorCode>(
e: unknown,
/**
* When supplied, this function will require that the input is a {@link SolanaError} _and_ that
* its error code is exactly this value.
*/
code?: TErrorCode,
): e is SolanaError<TErrorCode> {
const isSolanaError = e instanceof Error && e.name === 'SolanaError';
Expand All @@ -22,8 +68,21 @@ type SolanaErrorCodedContext = Readonly<{
};
}>;

/**
* Encapsulates an error's stacktrace, a Solana-specific numeric code that indicates what went
* wrong, and optional context if the type of error indicated by the code supports it.
*/
export class SolanaError<TErrorCode extends SolanaErrorCode = SolanaErrorCode> extends Error {
/**
* Indicates the root cause of this {@link SolanaError}, if any.
*
* For example, a transaction error might have an instruction error as its root cause. In this
* case, you will be able to access the instruction error on the transaction error as `cause`.
*/
readonly cause?: TErrorCode extends SolanaErrorCodeWithCause ? SolanaError : unknown = this.cause;
/**
* Contains context that can assist in understanding or recovering from a {@link SolanaError}.
*/
readonly context: SolanaErrorCodedContext[TErrorCode];
constructor(
...[code, contextAndErrorOptions]: SolanaErrorContext[TErrorCode] extends undefined
Expand Down
65 changes: 65 additions & 0 deletions packages/errors/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
/**
* This package brings together every error message across all Solana JavaScript modules.
*
* # Reading error messages
*
* ## In development mode
*
* When your bundler sets the constant `__DEV__` to `true`, every error message will be included in
* the bundle. As such, you will be able to read them in plain language wherever they appear.
*
* > [!WARNING]
* > The size of your JavaScript bundle will increase significantly with the inclusion of every
* > error message in development mode. Be sure to build your bundle with `__DEV__` set to `false`
* > when you go to production.
*
* ## In production mode
*
* When your bundler sets the constant `__DEV__` to `false`, error messages will be stripped from
* the bundle to save space. Only the error code will appear when an error is encountered. Follow
* the instructions in the error message to convert the error code back to the human-readable error
* message.
*
* For instance, to recover the error text for the error with code `123`:
*
* ```package-install
* npx @solana/errors decode -- 123
* ```
*
* > [!IMPORTANT]
* > The string representation of a {@link SolanaError} should not be shown to users. Developers
* > should use {@link isSolanaError} to distinguish the type of a thrown error, then show a custom,
* > localized error message appropriate for their application's audience. Custom error messages
* > should use the error's {@link SolanaError#context | `context`} if it would help the reader
* > understand what happened and/or what to do next.
Comment on lines +29 to +34
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this.

*
* # Adding a new error
*
* 1. Add a new exported error code constant to `src/codes.ts`.
* 2. Add that new constant to the {@link SolanaErrorCode} union in `src/codes.ts`.
* 3. If you would like the new error to encapsulate context about the error itself (eg. the public
* keys for which a transaction is missing signatures) define the shape of that context in
* `src/context.ts`.
* 4. Add the error's message to `src/messages.ts`. Any context values that you defined above will
* be interpolated into the message wherever you write `$key`, where `key` is the index of a
* value in the context (eg. ``'Missing a signature for account `$address`'``).
* 5. Publish a new version of `@solana/errors`.
* 6. Bump the version of `@solana/errors` in the package from which the error is thrown.
*
* # Removing an error message
*
* - Don't remove errors.
* - Don't change the meaning of an error message.
* - Don't change or reorder error codes.
* - Don't change or remove members of an error's context.
*
* When an older client throws an error, we want to make sure that they can always decode the error.
* If you make any of the changes above, old clients will, by definition, not have received your
* changes. This could make the errors that they throw impossible to decode going forward.
*
* # Catching errors
*
* See {@link isSolanaError} for an example of how to handle a caught {@link SolanaError}.
*
* @packageDocumentation
*/
export * from './codes';
export * from './error';
export * from './json-rpc-error';
Expand Down
3 changes: 3 additions & 0 deletions packages/errors/src/instruction-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ const ORDERED_ERROR_NAMES = [
];

export function getSolanaErrorFromInstructionError(
/**
* The index of the instruction inside the transaction.
*/
index: bigint | number,
instructionError: string | { [key: string]: unknown },
): SolanaError {
Expand Down
5 changes: 4 additions & 1 deletion packages/errors/src/json-rpc-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ interface RpcErrorResponse {

type TransactionError = string | { [key: string]: unknown };

// Keep in sync with https://github.com/anza-xyz/agave/blob/master/rpc-client-api/src/response.rs
/**
* Keep in sync with https://github.com/anza-xyz/agave/blob/master/rpc-client-api/src/response.rs
* @hidden
*/
export interface RpcSimulateTransactionResult {
accounts:
| ({
Expand Down
14 changes: 9 additions & 5 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors#adding-a-new-error
*
* WARNING:
* - Don't change the meaning of an error message.
*/
import {
SOLANA_ERROR__ACCOUNTS__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__ACCOUNTS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED,
Expand Down Expand Up @@ -227,11 +234,8 @@ import {
} from './codes';

/**
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors#adding-a-new-error
*
* WARNING:
* - Don't change the meaning of an error message.
* A map of every {@link SolanaError} code to the error message shown to developers in development
* mode.
*/
export const SolanaErrorMessages: Readonly<{
// This type makes this data structure exhaustive with respect to `SolanaErrorCode`.
Expand Down
3 changes: 2 additions & 1 deletion packages/errors/typedoc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "https://typedoc.org/schema.json",
"extends": ["../typedoc.base.json"],
"entryPoints": ["src/index.ts"]
"entryPoints": ["src/index.ts"],
"readme": "none"
}
Loading