The current version of ethers as of this article is 5.2.0.

Ahoy!
This highlights article is a bit earlier than usual (I’m targetting every 3-ish months), but it seemed like a good time to introduce some of the changes in v5.2, which are a bit more substantial and might benefit from a little extra explaining.
Also, by making a minor version bump I was also able to spruce up some other functionality that required slight changes to the signatures, in backwards-compatible ways.
Custom Contract Error
Solidity recently added support for custom contract errors, which allow for more expressive errors than the existing Error(string message)
type.
There isn’t much to say beyond, this is awesome! It has been a long-awaited feature, but is available as of Solidity 0.8.4.
And that now ethers supports them for all your error handling needs and desires, in both JSON ABIs and Human-Readable ABIs. and exposes the error details in thrown CALL_EXCEPTION
errors.
Example:
// test.sol
contract Test {
error SomeCustomeError(address addr, uint256 balance); mapping (address => uint) _balances; function throwIfNotZero(address owner) pure returns (bool) {
if (_balances[owner] > 0) {
revert SomeCustomError(owner, _balances[owner]);
}
return true;
} // ... more code here ...}
The coresponding JavaScript (in ethers):
// example.js
const abi = [
"function throwIfNotZero(address owner) pure returns (bool)",
"error SomeCustomError(address addr, uint256 value)"
];const contract = new Contract(someAddress, abi, provider);
try {
const result = await contract.throwIfNotZero(addr);
console.log("Result:", result);} catch (e) {
if (e.code === Logger.errors.CALL_EXCEPTION) { // If the error was SomeCustomError(), we can get the args...
if (e.errorName === "SomeCustomError") { // These are both the same; keyword vs positional.
console.log(e.errorArgs.addr);
console.log(e.errorArgs[0]); // These are both the same; keyword vs positional
console.log(e.errorArgs.value);
console.log(e.errorArgs[1]);
}
}
}
Note: this mainly benefits constant contract methods or when using the callStatic technique of analyzing state-mutating contract methods, since transactions still cannot return their error data in a reverted (status = 0) transaction.
Detecting Replacement Transactions
A common problem many developers have to deal with is users changing their minds after submitting a transaction.
With the current surges and volatile gas prices, many users after submitting a transaction, may decide to increase the gasPrice to get their transaction in faster (or at all). But changing the gasPrice results in a different transaction hash, which is bad if your application is waiting indefinitely for that hash; ideally you would like to know the new hash and keep on truckin’.
MetaMask (and similar clients) also supports cancelling a transaction. The Ethereum network doesn’t explicitly support cancelling a transaction already in the mempool, but it can be approximated by sending a new transaction with the same nonce
, but with a higher gasPrice
(with the value
and data
set to zero, and the to
set to from
), effectively bribing miners to ignore the initial transaction and focus on the new one. Once this new transaction is mined, the original transaction has become invalid and therefore cancelled.
Sometimes, a user may just forget about a stale transaction that was under-priced and later make another, completely unrelated transaction which satisfies the current gas price, replacing the earlier stale transaction. This is ultimately the same as cancelling a transaction, but less explicit.
So, a new feature has been added to help developers detect and course-correct their applications when things go awry.
When using tx.wait()
on a transaction made from the sendTransaction()
method of a Signer (including non-constant methods on Contract), if the transaction is repriced or cancelled, a TRANSACTION_REPLACED
error is thrown along with some extra details of what happened, allowing for appropriate “next steps” to be taken.
Example:
// Send the transaction
const tx = await contract.transfer(to, amount);try {
// Wait for the transaction to be mined
const receipt = await tx.wait(); // The transactions was mined without issue
myProcessMinedTransaction(tx, receipt);} catch (error) {
if (error.code === Logger.errors.TRANSACTION_REPLACED) { if (error.cancelled) {
// The transaction was replaced :'(
myProcessCancelledTransaction(tx, error.replacement); } else { // The user used "speed up" or something similar
// in their client, but we now have the updated info
myProcessMinedTransaction(error.replacement, error.receipt);
}
}
}
The error object in the event of a TRANSACTION_REPLACED
error has a few useful properties to simplify further processing:
Error {
code: "TRANSACTION_REPLACED", // The reason why the transaction was replaced
// - "repriced" is generally nothing of concern, the
// only difference in the transaction is the gasPrice
// - "cancelled" means the `to` has been set to the `from`,
// the data has been set to `0x` and value set to 0
// - "replaced" means that the transaction is unrelated to
// the original transaction
reason: "repriced" | "cancelled" | "replaced", // This is a short-hand property as the effects of either a
// "cancelled" or "replaced" tx are effectively cancelled
cancelled: (reason === "cancelled" || reason === "replaced"), // The TransactionResponse which replaced the original
replacement: [ the replacement transaction response ] // The TransactionReceipt of the replacement transaction
receipt: [ the receipt for the replacement transaction ],
}
NOTE: if a transaction is repriced or cancelled in the client, the original transaction may still be mined instead of the replacement, in which case the original code path is followed and the catch
is never entered
Some house cleaning…
- Several simple signature changes that bring the type-checking more in-line with the implementations, such as BigNumberish incuding bigint and filters allowing
null
topics - The EtherscanProvider has been redesigned to make sub-classing more flexible, so their BscScan explorer APIs can be easily integrated; the ancillary package will be available soon
- The FixedNumber formatted string and unit formatting no longer include decimal points for types which have zero decimal places (types with 1 or more decimal places will still always include a decimal and at least one whole and one decimal value) as well as parsing is now more forgiving if superfluous zeros are present
- A small collection of other small improvements that jived well backwards-compatible signature changes
As always, to see all the nitty gritty details, the Changelog is automatically managed by the publish scripts, with links to the changes and issues they resolve.
That’s all folks…
If you have any comments or questions, feel free to reach out to me on Twitter (via public Tweets, or DMs are open) or on GitHub (e-mail or ethers discussions).
Happy coding and thanks for reading! :)
Who is this General Failure, and why are they reading my harddrive?