Mesh LogoMesh
ResourcesChallenges

Transaction Failures

Fix Cardano transaction failures with Mesh SDK's automatic UTXO selection and fee calculation.

Cardano transactions fail due to UTXO conflicts, insufficient inputs, incorrect fees, or size limits. Mesh eliminates most failures by automatically handling UTXO selection, fee estimation, and change calculation.

Common failure causes

CauseDescription
UTXO conflictsAnother transaction consumed the UTXO you selected
Insufficient fundsUTXOs don't sum to required value
Transaction too largeExceeds 16KB size limit
Fee miscalculationChange output doesn't account for correct fee
Invalid changeChange output below minimum UTXO value
Token entanglementADA locked with tokens you can't move

Why transactions fail

UTXO conflicts

When you build a transaction, you select specific UTXOs. If another transaction consumes one first, your transaction becomes invalid.

Common scenarios:

  • User clicks submit multiple times
  • Backend processes concurrent requests
  • Multiple browser tabs share the same wallet

Insufficient funds with adequate balance

CauseWhy it happens
Locked tokensADA is paired with native tokens
Minimum UTXOOutputs must meet minimum value requirements
Fee reductionFees reduce available balance
FragmentationMany small UTXOs create oversized transactions

Fee calculation complexity

Fees depend on transaction size, which depends on inputs, outputs, and witnesses. This circular dependency requires iterative solving.


The solution

MeshTxBuilder handles all complexity automatically.

Quick start

npm install @meshsdk/core @meshsdk/react

Basic usage

import { MeshTxBuilder, BlockfrostProvider } from "@meshsdk/core";
import { MeshCardanoBrowserWallet } from "@meshsdk/wallet";

const provider = new BlockfrostProvider("<your-api-key>");
const wallet = await MeshCardanoBrowserWallet.enable("eternl");

const txBuilder = new MeshTxBuilder({
  fetcher: provider,
  submitter: provider,
});

const unsignedTx = await txBuilder
  .txOut(recipientAddress, [{ unit: "lovelace", quantity: "5000000" }])
  .changeAddress(await wallet.getChangeAddressBech32())
  .selectUtxosFrom(await wallet.getUtxosMesh())
  .complete();

const signedTx = await wallet.signTx(unsignedTx, false);
const txHash = await wallet.submitTx(signedTx);

What Mesh handles automatically

FeatureWhat it does
UTXO selectionChooses optimal inputs minimizing size
Fee calculationIteratively calculates exact fee
Change handlingCreates valid change outputs
Token routingRoutes tokens to change if not sent
ValidationCatches errors before submission

Handle remaining failures

Even with Mesh, some failures require application-level handling.

Implement retry logic

async function submitWithRetry(wallet, signedTx, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await wallet.submitTx(signedTx);
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
    }
  }
}

Fetch fresh UTXOs

Always fetch UTXOs immediately before building:

// Wrong: stale UTXOs
const cachedUtxos = await wallet.getUtxosMesh();
// ... time passes ...
const tx = await txBuilder.selectUtxosFrom(cachedUtxos).complete(); // May fail

// Correct: fresh UTXOs at transaction time
async function buildTransaction() {
  const freshUtxos = await wallet.getUtxosMesh();
  return await txBuilder.selectUtxosFrom(freshUtxos).complete();
}

Prevent double-submission

const [isSubmitting, setIsSubmitting] = useState(false);

async function handleSubmit() {
  if (isSubmitting) return;
  setIsSubmitting(true);
  try {
    const txHash = await submitTransaction();
  } catch (error) {
    showError(error.message);
  } finally {
    setIsSubmitting(false);
  }
}

Handle errors gracefully

try {
  const tx = await txBuilder
    .txOut(recipientAddress, amount)
    .changeAddress(changeAddress)
    .selectUtxosFrom(utxos)
    .complete();

  const signedTx = await wallet.signTx(tx);
  const txHash = await wallet.submitTx(signedTx);
} catch (error) {
  if (error.message.includes("Insufficient")) {
    showError("Not enough ADA to complete this transaction");
  } else if (error.message.includes("User")) {
    showInfo("Transaction cancelled");
  } else {
    showError("Transaction failed. Please try again.");
  }
}

Advanced patterns

Multi-asset transactions

const tx = await txBuilder
  .txOut(recipient, [
    { unit: "lovelace", quantity: "2000000" },
    { unit: policyId + assetName, quantity: "1" }
  ])
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Smart contract transactions

const tx = await txBuilder
  .spendingPlutusScript(languageVersion)
  .txIn(scriptUtxo.input.txHash, scriptUtxo.input.outputIndex)
  .txInScript(scriptCbor)
  .txInDatumValue(datum)
  .txInRedeemerValue(redeemer)
  .txOut(outputAddress, outputValue)
  .txInCollateral(collateralUtxo.input.txHash, collateralUtxo.input.outputIndex)
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Best practices

PracticeReason
Fetch fresh UTXOsAvoid stale data and conflicts
Implement retry logicHandle transient failures
Disable button during submitPrevent double-submission
Show clear error messagesHelp users understand failures
Test on preprod firstCatch issues before mainnet

On this page