ERC721 V5 custom error rendering in client front. No Returns a renderable string

I am having problems rendering custom error on the front end with the new v5 version of contracts.
I used the ethers.js staticCall function in my front, to be able to capture the errors in a simple way:

try {
		await contractContainer.safeTransferFrom.staticCall(from, to, idNFT);
		const transaction = await contractContainer.safeTransferFrom(from, to, idNFT);
		const ok = await transaction.wait();

But I notice that it does not return a proper error string, but the print error function:

Error: Error: execution reverted: ERC721InsufficientApproval(address,uint256)

What is the best way to render a custom error like the following?
revert ERC721InsufficientApproval(spender, tokenId);

Unless I'm missing something, your question is a mess.

First off, you're mixing offchain perspective (Javascript code) with onchain perspective (Solidity code), making it very difficult to understand what you're even trying to ask.

Second, transferFrom (and likewise safeTransferFrom) is typically NOT something that you call from the offchain.

Rather, you call someTokenContract.approve, and then you call someOtherContract.someFunc.

Following your second call, someFunc will internally call transferFrom (or safeTransferFrom).

BTW, the first error-message in your question - Insufficient Allowance - tells you exactly that.

Not a complete answer to your question I suppose, but like I said - it is rather difficult to understand what you're trying to ask...

I am sorry that for you my question is a mess for you.

I point out that my question is about rendering custom errors on the client or frontend, as I have no problems with the error handling you are discussing.

but, I am curious how you suggest that offchain no transfer is made.

How would you send an NFT to a marketplace contract with a dApp or how would you give the transfer option to a user in your dApp who wanted to send it to another?

I would like to point out that my query concerns the rendering of custom errors on the client. I am not getting a manageable string, as I am used to with errors in a typical require..
That is my question.
I will see how to be more clear for everyone.

There is no difference between require and revert in this aspect.

In fact, there is almost no difference at all between require and revert.

The line marked in your screenshot [and it would have been a lot easier for everyone, yourself included, if you had posted that as plaintext, since I cannot paste it here for clarity] is different from a "standard" require statement in the fact that it embeds two values into the error-message, so you probably need to adjust your client-side code to parse that error-message properly.

A solution I have found is with the use of the parseError function of the contract object interface.

capture the error in the try-catch and use the and decoding.
next, print name of the custom error and then, if it comes with arguments, I render them in this way:

} catch (err) {
		console.log('Error: ', err);
		res.state = 'Error';
		const decodedError = contract.interface.parseError(;
		console.log(`Transaction failed: `, decodedError);
		res.message = `${decodedError?.name } ${decodedError?.args}` || "Unknown error";
		res = res;
		return res;

See my (updated) answer above.

Which two values does the error return?
In the argsI get the type of the argument.

Error:  Error: execution reverted: ERC721InsufficientApproval(address,uint256)
    at makeError (errors.ts:694:21)
    at getBuiltinCallException (abi-coder.ts:118:12)
    at _AbiCoder.getBuiltinCallException (abi-coder.ts:230:16)
    at _Interface.makeError (interface.ts:925:32)
    at staticCallResult (contract.ts:340:42)
    at async AsyncFunction.staticCall (contract.ts:303:24)
    at async sendContainer (nfts-functions.js?t=1701630113623:299:3)
    at async HTMLButtonElement.transferContainer (admin.svelte:250:11)
Reason ERC721InsufficientApproval(address,uint256)

@barakman I have simplified the question as much as possible for the sake of clarity by eliminating all my speculations. I hope it is now clear to everyone.

These two arguments:

revert ERC721InsufficientApproval(spender, tokenId);

The error-message mentioned further down in your last comment doesn't even appear in the original question!

Again, I strongly recommend that you edit your question and write it down in a proper manner, including ALL the relevant details and ONLY the relevant details in PLAINTEXT.

Consider the fact that adding new information along the comment-thread is extremely confusing and unhelpful for the purpose of getting an accurate answer to your question.

The error is indifferent because the query is about your rendering on the client.
The problem is not the error returned by the function but how to render it properly on the client.

However I will remove the images that bother you so much and replicate the same error.

One of a possible solutions is to create some interfaces with all the custom error and capture the that if it is received equally using static calls as if not.

export const sendContainer = async (from, to, idNFT) => {
	const { contractContainer } = await contractNFTSigner();
	let res = { message: 'No blockchain call', state: 'Error' };
	try {
		await contractContainer.safeTransferFrom.staticCall(from, to, idNFT);
		const transaction = await contractContainer.safeTransferFrom(from, to, idNFT);
		const ok = await transaction.wait();

		if (ok) {
			res.message = `Ha mandado su Container con id ${idNFT} a la dirección ${to}`;
			res.state = 'Sucess';
			res = res;
			return res;
	} catch (err) {
		console.log('Error: ', err);
		const decodedError = contractContainer.interface.parseError(;
		console.log(`Error: `, decodedError);
		res.message = `${decodedError?.name } ${decodedError?.args}` || "Unknown Error";
		res = res;
		return res;

@barakman I hope that the answer is now to your liking.

Although it is a topic for perhaps another thread you suggested me in this post not to make direct calls from my front end to the safeTransferFrom function and use an intermediary contract if I have not misunderstood you.

I imagine you mean something like a mint function like this:

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);

But in the case of the safeTransferFrom function it is a public visivility it can be called by other contracts or dApps without problems.

Why did you recommend me to use an intermediary contract then?

Despite the name dApp - Decentralized Application - it is actually a very much centralized entity, which means that it is in the hands of someone who can choose to change it at any time, with or without anyone's approval, hence it is not to be trusted.

When a dApp is allowed to transfer funds from John's wallet, there is no trusted way for John to tell what the dApp will do with those funds, so it would be very unwise for John to allow the dApp to transfer funds from his wallet.

When a contract is allowed to transfer funds from John's wallet, there IS a trusted way for John to tell what the contract will do with those funds, by looking into the contract's code, which resides on a publicly visible database (aka blockchain), which cannot be altered, manipulated or tampered with.

This is essentially the difference between Web2 and Web3.

But anyway, I take back my statement about transferFrom not being meant for use by dApps. Hell, meta-mask transfers from its users all the time, and it is probably the most widely-used dApp in the ecosystem.

But I just cannot see why a dApp would ever need to call safeTransferFrom.

In fact, a dApp shouldn't even be able to call safeTransferFrom.

This function is part of an internal contract mechanism allowing that contract to safely execute transferFrom on an ERC20 token contract which does not comply with the ERC20 standard (a mechanism which is obviously implemented in the calling contract and not in the token contract).

So no one outside the contract (i.e., other contracts, and most certainly dApps) should even be able to call that function to begin with.

And frankly, as far as I remember, a dApp doesn't even need this mechanism to begin with, because calling an ERC20 non-compliant transferFrom using a standard ERC20 ABI causes the transaction to revert only when executed on-chain (inside a contract function), not off-chain (from a script).

Hi, thanks for the feedback.

I think there is some confusion. I am working with ERC721 contracts, they are transfers of NFTs.

It is not about this function:

function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, value);
        _transfer(from, to, value);
        return true;

Otherwise from safeTransferFrom, which only exists in ERC721, and is not present in ERC20:

nterface IERC20:

totalSupply(), balanceOf(account), transfer(to, value), allowance(owner, spender), approve(spender, value), transferFrom(from, to, value).

Whereby, in order to perform a token ERC20 transfer, it is necessary to approve the submission and then execute the transfer after.

The main function is this one, which validates the NFT property before performing the transfer, and which has public visibility:

 function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
        transferFrom(from, to, tokenId);
        _checkOnERC721Received(from, to, tokenId, data);

Therefore I think you are wrong when you state that:
This function is part of an internal contract mechanism that allows such a contract to execute transferFrom securely on an ERC20 token contract that does not meet the ERC20 standard."

It is a public function to send NFTs between users.
And I know of no other way to send an NFT from a dApp by a user to another.

The security is that if you don't delegate ownership of the NFT you will have to sign the send order. The user must put the address and sign the transaction as this cannot be done automatically, with the exception of NFTs marketplace intermediary contracts, who are the ones sending the NFTs, either because they are the temporary owners of the crypto assets or have delegated ownership for such uses.

My comments throughout this thread are with respect to ERC20.
So they are possibly wrong with respect to ERC721.