Skip to main content

Overview

The NEAR API TypeScript library uses a sophisticated error handling system called NatError. It provides strongly-typed, structured errors with full context, enabling precise error handling and debugging.

The NatError System

NatError Class

class NatError<K extends NatErrorKind> extends Error {
  public readonly kind: K;
  public readonly context: ContextFor<K>;
  
  constructor(args: { kind: K; context: ContextFor<K> }) {
    super(`<${args.kind}>`);
    this.name = 'NatError';
    this.kind = args.kind;
    this.context = args.context;
  }
}
Every NatError has two key properties:
  • kind: A string identifier for the error type (e.g., 'Client.GetAccountInfo.Rpc.Account.NotFound')
  • context: Typed contextual data specific to the error kind

Error Hierarchy

Errors are organized in a hierarchical namespace:
Component.Method.Layer.ErrorType
│         │      │     └─ Specific error
│         │      └─────── Layer (Args, Rpc, Internal)
│         └────────────── Method name
└──────────────────────── Component (Client, MemorySigner, etc.)
Examples:
  • Client.GetAccountInfo.Rpc.Account.NotFound
  • MemorySigner.ExecuteTransaction.KeyPool.SigningKey.NotFound
  • CreateMemoryKeyService.Args.InvalidSchema
  • MemoryKeyService.SignTransaction.Internal

Error Registries

Each component defines its error registry using TypeScript interfaces:
interface ClientPublicErrorRegistry {
  'CreateClient.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateClient.Internal': InternalErrorContext;
  'Client.GetAccountInfo.Args.InvalidSchema': InvalidSchemaErrorContext;
  'Client.GetAccountInfo.Rpc.Account.NotFound': { accountId: string };
  'Client.GetAccountInfo.Rpc.Block.NotFound': { blockReference: BlockReference };
  // ... more errors
}
The registry provides:
  • Error kinds: All possible error strings
  • Context types: What data each error includes
  • Type safety: Compiler ensures correct error handling

Safe vs Throwable Variants

Every operation in the library comes in two flavors:

Throwable Variants

Traditional functions that throw errors:
try {
  const client = createClient({ transport: config });
  const info = await client.getAccountInfo({ accountId: 'alice.near' });
  console.log(info);
} catch (error) {
  console.error(error);
}

Safe Variants

Return a Result<T, E> type for explicit error handling:
const clientResult = safeCreateClient({ transport: config });

if (!clientResult.ok) {
  console.error('Client creation failed:', clientResult.error.kind);
  return;
}

const client = clientResult.value;

const infoResult = await client.safeGetAccountInfo({ 
  accountId: 'alice.near' 
});

if (!infoResult.ok) {
  console.error('Get account info failed:', infoResult.error.kind);
  return;
}

const info = infoResult.value;

Result Type

The Result type represents either success or failure:
type ResultOk<V> = { ok: true; value: V };
type ResultErr<E> = { ok: false; error: E };
type Result<V, E> = ResultOk<V> | ResultErr<E>;

Working with Results

const result = await client.safeGetAccountInfo({ accountId: 'alice.near' });

// Type guard
if (result.ok) {
  // result.value is available
  console.log(result.value.amount);
} else {
  // result.error is available
  console.log(result.error.kind);
  console.log(result.error.context);
}

Error Categories

Schema Validation Errors

These occur when arguments don’t match the expected schema:
const result = await client.safeGetAccountInfo({
  accountId: 'alice.near',
  // @ts-expect-error - invalid field
  invalidField: 123
});

if (!result.ok && 
    result.error.kind === 'Client.GetAccountInfo.Args.InvalidSchema') {
  const zodError = result.error.context.zodError;
  console.log('Validation failed:', zodError.issues);
}
Context type:
type InvalidSchemaErrorContext = {
  zodError: $ZodError;  // Detailed validation errors
};

RPC Errors

Errors from the NEAR RPC endpoint:
const result = await client.safeGetAccountInfo({
  accountId: 'nonexistent.near'
});

if (!result.ok && 
    result.error.kind === 'Client.GetAccountInfo.Rpc.Account.NotFound') {
  console.log('Account does not exist:', result.error.context.accountId);
}

Transaction Errors

Errors during transaction execution:
const result = await signer.safeExecuteTransaction({
  intent: {
    receiverAccountId: 'nonexistent.near',
    action: transfer({ amount: near('1') })
  }
});

if (!result.ok && 
    result.error.kind === 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Receiver.NotFound') {
  console.log('Receiver does not exist:', result.error.context.accountId);
}

Key Service Errors

Errors from key management operations:
const result = await keyService.safeSignTransaction({
  transaction: {
    signerPublicKey: 'ed25519:UnknownKey...',
    // ... other fields
  }
});

if (!result.ok && 
    result.error.kind === 'MemoryKeyService.SignTransaction.SigningKeyPair.NotFound') {
  console.log('No private key for:', result.error.context.signerPublicKey);
  console.log('Available keys:', Object.keys(keyService.keyPairs));
}

Signer Errors

Errors from the signing process:
const result = await signer.safeExecuteTransaction({
  intent: myIntent
});

if (!result.ok && 
    result.error.kind === 'MemorySigner.ExecuteTransaction.KeyPool.Empty') {
  console.log('No access keys available');
  console.log('Ensure the account has access keys and KeyService has matching private keys');
}

Internal Errors

Unexpected errors that indicate bugs or system issues:
if (!result.ok && result.error.kind.endsWith('.Internal')) {
  console.error('Internal error occurred:', result.error.kind);
  console.error('Cause:', result.error.context.cause);
  // Report this as a bug
}
Context type:
type InternalErrorContext = {
  cause: unknown;  // Original error that was caught
};

Error Checking Utilities

isNatError

Check if an error is a NatError:
import { isNatError } from '@near-api/client';

try {
  await signer.executeTransaction({ intent });
} catch (error) {
  if (isNatError(error)) {
    console.log('NatError kind:', error.kind);
    console.log('NatError context:', error.context);
  } else {
    console.log('Unknown error:', error);
  }
}

isNatError with Kind

Check for a specific error kind:
try {
  await client.getAccountInfo({ accountId: 'alice.near' });
} catch (error) {
  if (isNatError(error, 'Client.GetAccountInfo.Rpc.Account.NotFound')) {
    console.log('Account not found:', error.context.accountId);
  }
}

isNatErrorOf

Check if error matches any of multiple kinds:
import { isNatErrorOf } from '@near-api/client';

try {
  await signer.executeTransaction({ intent });
} catch (error) {
  if (isNatErrorOf(error, [
    'MemorySigner.ExecuteTransaction.Rpc.Transaction.Receiver.NotFound',
    'MemorySigner.ExecuteTransaction.Rpc.Transaction.Signer.Balance.TooLow',
    'MemorySigner.ExecuteTransaction.KeyPool.SigningKey.NotFound'
  ])) {
    console.log('Transaction failed with expected error:', error.kind);
    // Handle these specific errors
  } else {
    console.log('Unexpected error:', error.kind);
    // Report or handle differently
  }
}

Error Handling Patterns

Pattern 1: Exhaustive Error Handling

Handle all known error types explicitly:
const result = await signer.safeExecuteTransaction({ intent });

if (!result.ok) {
  switch (result.error.kind) {
    case 'MemorySigner.ExecuteTransaction.Args.InvalidSchema':
      console.log('Invalid arguments:', result.error.context.zodError);
      break;
    
    case 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Receiver.NotFound':
      console.log('Receiver not found:', result.error.context.accountId);
      break;
    
    case 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Signer.Balance.TooLow':
      console.log('Insufficient balance');
      break;
    
    case 'MemorySigner.ExecuteTransaction.KeyPool.SigningKey.NotFound':
      console.log('No valid signing key');
      break;
    
    default:
      console.log('Unhandled error:', result.error.kind);
      console.log('Context:', result.error.context);
  }
}

Pattern 2: Category-Based Handling

Group errors by category:
const result = await client.safeGetAccountInfo({ accountId });

if (!result.ok) {
  const { kind, context } = result.error;
  
  if (kind.includes('.Args.InvalidSchema')) {
    // Handle validation errors
    console.log('Invalid arguments');
  } else if (kind.includes('.Rpc.')) {
    // Handle RPC errors
    console.log('RPC error:', kind);
  } else if (kind.includes('.Internal')) {
    // Handle internal errors
    console.error('Internal error:', context.cause);
  }
}

Pattern 3: Early Return Pattern

Return early on errors for cleaner code:
async function processAccount(accountId: string) {
  const infoResult = await client.safeGetAccountInfo({ accountId });
  if (!infoResult.ok) {
    console.error('Failed to get account info:', infoResult.error.kind);
    return null;
  }
  
  const keysResult = await client.safeGetAccountAccessKeys({ accountId });
  if (!keysResult.ok) {
    console.error('Failed to get access keys:', keysResult.error.kind);
    return null;
  }
  
  // Process data
  return {
    info: infoResult.value,
    keys: keysResult.value
  };
}

Pattern 4: Error Recovery

Attempt recovery based on error type:
const result = await signer.safeExecuteTransaction({ intent });

if (!result.ok) {
  if (result.error.kind === 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Receiver.NotFound') {
    // Receiver doesn't exist, create it first
    console.log('Creating receiver account first...');
    
    await signer.executeTransaction({
      intent: {
        receiverAccountId: intent.receiverAccountId,
        actions: [
          createAccount(),
          transfer({ amount: near('1') })
        ]
      }
    });
    
    // Retry original transaction
    return await signer.executeTransaction({ intent });
  }
  
  // Can't recover from other errors
  throw result.error;
}

Complete Error Registry

The library defines a comprehensive error registry covering all components:
interface NatPublicErrorRegistry {
  // Client errors
  'CreateClient.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateClient.Internal': InternalErrorContext;
  'Client.GetAccountInfo.Args.InvalidSchema': InvalidSchemaErrorContext;
  'Client.GetAccountInfo.Rpc.Account.NotFound': { accountId: string };
  'Client.GetAccountInfo.Rpc.Block.NotFound': { blockReference: BlockReference };
  'Client.GetAccountAccessKey.Rpc.AccessKey.NotFound': { accountId: string; publicKey: PublicKey };
  // ... more Client errors
  
  // MemoryKeyService errors
  'CreateMemoryKeyService.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateMemoryKeyService.Internal': InternalErrorContext;
  'MemoryKeyService.SignTransaction.SigningKeyPair.NotFound': { signerPublicKey: PublicKey };
  'MemoryKeyService.FindKeyPair.NotFound': { publicKey: PublicKey };
  // ... more KeyService errors
  
  // MemorySigner errors
  'CreateMemorySigner.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateMemorySigner.Internal': InternalErrorContext;
  'MemorySigner.ExecuteTransaction.KeyPool.Empty': {};
  'MemorySigner.ExecuteTransaction.KeyPool.SigningKey.NotFound': {};
  'MemorySigner.ExecuteTransaction.Rpc.Transaction.Receiver.NotFound': { accountId: string };
  'MemorySigner.ExecuteTransaction.Rpc.Transaction.Signer.Balance.TooLow': {};
  // ... more Signer errors
  
  // Action errors
  'CreateAction.Transfer.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateAction.FunctionCall.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateAction.FunctionCall.SerializeArgs.Failed': { cause: unknown; functionArgs: unknown };
  // ... more Action errors
  
  // Token and Gas errors
  'CreateNearToken.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateNearGas.Args.InvalidSchema': InvalidSchemaErrorContext;
  // ... more utility errors
  
  // KeyPair errors
  'CreateKeyPair.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateKeyPair.Internal': InternalErrorContext;
}

Best Practices

Always Use Safe Variants in Production

Use safe variants (safeExecuteTransaction, safeGetAccountInfo) for explicit error handling. Reserve throwable variants for prototyping and testing.

Handle Expected Errors Explicitly

For business-critical operations, handle all expected error types explicitly with switch statements.

Log Internal Errors

Always log internal errors with full context. These indicate bugs or system issues that need investigation.

Provide User-Friendly Messages

Convert error kinds into user-friendly messages:
function getUserMessage(error: NatError<any>): string {
  switch (error.kind) {
    case 'Client.GetAccountInfo.Rpc.Account.NotFound':
      return `Account "${error.context.accountId}" does not exist`;
    case 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Signer.Balance.TooLow':
      return 'Insufficient balance to complete transaction';
    default:
      return 'An unexpected error occurred';
  }
}

Don't Swallow Errors

Never catch errors without handling or logging them. Use safe variants to make error handling explicit.