Debugging Shopify Functions in Practice


This is a continually-updated journal of errors and bugs I’ve run into while developing my Shopify app, Regios Automatic Discounts, using Shopify Functions.

Hopefully, this can help at least one person find the solution to their problem.

If you have any questions in the meantime, hit me up on Twitter.

How can I debug my Shopify Functions in production?

Debugging a Function is easy on a development store, but if your function is running into issues in production, the only way you can troubleshoot it by asking a merchant to send you recent error reports. However, merchants only have this option if your function actually crashed, not if it only produced incorrect output.

So, how do you work around this? I chose to add a feature called “Developer options” to my app, where my users can toggle flag that forces the discount to crash every time it executes. When this flag is on, you can then receive error reports from merchants, and use the input query to test your function locally until everything works properly.

Because it’s very easy for a merchant to mistakenly enable this feature and completely break their discount, I blocked this feature with a very scary warning modal that appears any time they try to open developer options. So far, I haven’t run into any issues with merchants accidentally turning on the “Always throw” developer option.

What is a RunOutOfFuelError, and what causes it?

A nightmare – pages upon pages of RunOutOfFuelError logs!

Last night, while I was troubleshooting my discount app on a user’s site, I stumbled upon a nightmare in my extension logs. To my horror, my discount app had crashed on one site almost 120 times in 24 hours with a RunOutOfFuelError, and at least 10 times on another. I immediately sent out an email to both stores, in order to request error logs.

A RunOutOfFuelError occurs when your Shopify Function executes more than 11 million instructions, which is the limit Shopify places on your discounts and other functions.

While it’s highly unlikely you’ll actually write 11 million lines of code, if the bulk of your discount’s logic takes place in a loop, the instruction count can balloon. For example, my discount app loops over every CartLine in an order. The time complexity of this loop is O(n), so the more line items in the customer’s cart, the more instructions my discount runs.

To illustrate this concept, I created a minimal script that runs my discount function in function-runner, and continually increases the number of cart lines in the order, until the instruction limit reaches the limit. This table contains some of the results:

Number of Cart LinesMillions of Instructions Executed
15.838921
56.419962
107.151708
157.823343
208.952961
259.643317
3010.34075
3511.030137

I also took the liberty of plotting this out using Wolfram Alpha. As you can see in the graph below, the number of instructions is roughly linear to the number of cart lines:

This is okay for most stores, but for stores that sell wholesale, where orders are usually large, this poses serious problems.

If you’re running into this issue, upvote this discussion on Shopify’s GitHub, and chime in with a comment: https://github.com/Shopify/function-examples/discussions/329

Should you use JavaScript to write Shopify Functions?

Rust seems cool and all, but I’ve avoided learning it for as long as possible, because I’m just not that interested in learning a whole new language. Instead, I implemented my discount app function in TypeScript, which now can compile to WebAssembly using the javy toolchain.

In hindsight, this might have been a bad choice, although I don’t have the numbers to back this claim up yet.

As shown in my previous performance chart, processing just 1 cart line in my discount function executes over 5 million instructions. I haven’t been able to profile this using wasmtime yet, but I have to assume that a non-trivial percentage of those instructions are spent on the QuickJS interpreter, which is embedded in the final WASM binary, and used to execute JavaScript (compiled to bytecode).

I might end up having to write a new version of this function in C++ or Rust if I end up with more high-volume users. If and when I do so, I’ll update this blog post with a comparison of the number of instructions the function executes in.

Help! My Metafield Is Null

If you followed the Shopify Discounts Functions tutorial, then you probably have a function-configuration metafield that stores your discount’s settings. For example, your configuration might contain a list of product and variant IDs that the discount applies to. Unfortunately, there’s a gotcha that can occur if your configuration grows too big.

As mentioned in the Shopify Functions documentation, there’s a limit of 10kb on any metafield an input query can read. If you go over this… Your metafield will simply be null at runtime.

Preventing Bloated Configs

To avoid this from happening, I recommend 2 things:

First, when validating the user’s input, check the size of the serialized JSON you’ll be using as function-configuration. Here’s a JavaScript snippet you can use, assuming your configuration is a variable named config:

// This code snippet is public domain. :)
const configJson = JSON.stringify(config);
const sizeInBytes = new Blob([configJson]).size;
if (sizeInBytes > 10000) {
  // Prevent submission.
}

Secondly, try to avoid storing any data in the discount configuration if it’s only used for presentation. For example, don’t store product names, descriptions, or image URL’s. All this excess data will do is just bloat your configuration and waste precious space.

You can also consider separating parts of your configuration into separate metafields. For example, if you potentially have a ton of variants in a function configuration, consider splitting the list of variant IDs into its own separate metafield.

Furthermore, you can consider more compact ways of storing data. For example, if you have a metafield that holds a list of variant IDs, does every ID really need to have the format gid://shopify/ProductVariant/12345? Why not potentially just have a list of numbers, separated by commas: 12345, ...? Variant ID numbers are maybe 15 characters max, so you could store up to 666 (😈) variants in one metafield easily.

And if you go over that amount, you can have 2 variant list metafields, where one contains the overflow.