3.6.4. Compensation Function

In this chapter, you'll learn what a compensation function is and how to add it to a step.

What is a Compensation Function#

A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow.

For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow.

By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems.


How to add a Compensation Function?#

A compensation function is passed as a second parameter to the createStep function.

For example, create the file src/workflows/hello-world.ts with the following content:

src/workflows/hello-world.ts
4} from "@medusajs/framework/workflows-sdk"5
6const step1 = createStep(7  "step-1",8  async () => {9    const message = `Hello from step one!`10
11    console.log(message)12
13    return new StepResponse(message)14  },15  async () => {16    console.log("Oops! Rolling back my changes...")17  }18)

Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow.


Test the Compensation Function#

Create a step in the same src/workflows/hello-world.ts file that throws an error:

src/workflows/hello-world.ts
1const step2 = createStep(2  "step-2",3  async () => {4    throw new Error("Throwing an error...")5  }6)

Then, create a workflow that uses the steps:

src/workflows/hello-world.ts
7// steps...8
9const myWorkflow = createWorkflow(10  "hello-world", 11  function (input) {12  const str1 = step1()13  step2()14
15  return new WorkflowResponse({16    message: str1,17  })18})19
20export default myWorkflow

Finally, execute the workflow from an API route:

src/api/workflow/route.ts
5import myWorkflow from "../../../workflows/hello-world"6
7export async function GET(8  req: MedusaRequest,9  res: MedusaResponse10) {11  const { result } = await myWorkflow(req.scope)12    .run()13
14  res.send(result)15}

Run the Medusa application and send a GET request to /workflow:

Code
curl http://localhost:9000/workflow

In the console, you'll see:

  • Hello from step one! logged in the terminal, indicating that the first step ran successfully.
  • Oops! Rolling back my changes... logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently.

Pass Input to Compensation Function#

If a step creates a record, the compensation function must receive the ID of the record to remove it.

To pass input to the compensation function, pass a second parameter in the StepResponse returned by the step.

For example:

Code
1import { 2  createStep,3  StepResponse,4} from "@medusajs/framework/workflows-sdk"5
6const step1 = createStep(7  "step-1",8  async () => {9    return new StepResponse(10      `Hello from step one!`, 11      { message: "Oops! Rolling back my changes..." }12    )13  },14  async ({ message }) => {15    console.log(message)16  }17)

In this example, the step passes an object as a second parameter to StepResponse.

The compensation function receives the object and uses its message property to log a message.


Resolve Resources from the Medusa Container#

The compensation function receives an object second parameter. The object has a container property that you use to resolve resources from the Medusa container.

For example:

Code
1import { 2  createStep,3  StepResponse,4} from "@medusajs/framework/workflows-sdk"5import { ContainerRegistrationKeys } from "@medusajs/framework/utils"6
7const step1 = createStep(8  "step-1",9  async () => {10    return new StepResponse(11      `Hello from step one!`, 12      { message: "Oops! Rolling back my changes..." }13    )14  },15  async ({ message }, { container }) => {16    const logger = container.resolve(17      ContainerRegistrationKeys.LOGGER18    )19
20    logger.info(message)21  }22)

In this example, you use the container property in the second object parameter of the compensation function to resolve the logger.

You then use the logger to log a message.


Handle Errors in Loops#

NoteThis feature is only available after Medusa v2.0.5.

Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step:

Code
1// other imports...2import { promiseAll } from "@medusajs/framework/utils"3
4type StepInput = {5  ids: string[]6}7
8const step1 = createStep(9  "step-1",10  async ({ ids }: StepInput, { container }) => {11    const erpModuleService = container.resolve(12      ERP_MODULE13    )14    const prevData: unknown[] = []15
16    await promiseAll(17      ids.map(async (id) => {18        const data = await erpModuleService.retrieve(id)19
20        await erpModuleService.delete(id)21
22        prevData.push(id)23      })24    )25
26    return new StepResponse(ids, prevData)27  }28)

In the step, you loop over the IDs to retrieve the item's data, store them in a prevData variable, then delete them using the ERP Module's service. You then pass the prevData variable to the compensation function.

However, if an error occurs in the loop, the prevData variable won't be passed to the compensation function as the execution never reached the return statement.

To handle errors in the loop so that the compensation function receives the last version of prevData before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the StepResponse.permanentFailure function:

Code
1try {2  await promiseAll(3    ids.map(async (id) => {4      const data = await erpModuleService.retrieve(id)5
6      await erpModuleService.delete(id)7
8      prevData.push(id)9    })10  )11} catch (e) {12  return StepResponse.permanentFailure(13    `An error occurred: ${e}`,14    prevData15  )16}

The StepResponse.permanentFailure fails the step and its workflow, triggering current and previous steps' compensation functions. The permanentFailure function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function.

So, if an error occurs during the loop, the compensation function will still receive the prevData variable to undo the changes made before the step failed.

Was this chapter helpful?
Edit this page