Twilio Function Error Response

Here's a recommendation on how to format the error response frorm a Twilio Function.

tl;dr

Always format the error response yourself as the second argument to callback.

The docs

The Twilio docs give a grand total of this advice on the subject of error responses:

If you have encountered an exception in your code or otherwise want to indicate an error state, invoke the callback method with the error object or intended message as a single parameter:

return callback(error);

I believe this is insufficient. There are several ways to call the callback with an error response. Each is a bit different on the output.

Method 1 - Error as first arg

This is the recommended method from the docs.

callback(error);

Calling a Function with this implementation will yield:

 ❯ curl -X POST http://localhost:3000/myfunction -v
< HTTP/1.1 500 Internal Server Error
< Content-Type: application/json; charset=utf-8
{"message":"My Error Message","name":"Error","stack":"stackTraceUsuallyHere"}

This is ok, but it leaves a few things to be desired. There is no way to return non-500 error responses (eg, 400). The JSON shape is pre-determined and uncustomizable. There is no way to set CORS headers. And if requested by a web page, such as from within Twilio Flex, the Content-Type returned is text/html.

Customize the response

One can simply return JSON from Twilio functions:

callback(null, { some: 'response' })

For more control, one can also modify the response status code and headers. Let's encapsulate all that in a formatResponse function that sets CORS headers and allows parameters for the status code and the body:

function formatResponse(statusCode, body)  {
  const response = new global.Twilio.Response();
  response.setHeaders({
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET,PUT,POST,PATCH,DELETE,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
    "Content-Type": "application/json",
  });
  response.setStatusCode(statusCode);
  response.setBody(body);
  return response;
}

Method 2 - Error response as first arg

Now we can set the status code as 400 and call the callback with the error response as the first argument:

const error = new Error("Validation warning");
callback(formatResponse(400, { error }));

Called, this yields:

❯ curl -X POST http://localhost:3000/myfunction -v
< HTTP/1.1 500 Internal Server Error
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET,PUT,POST,PATCH,DELETE,OPTIONS
< Access-Control-Allow-Headers: Content-Type
< Content-Type: application/json; charset=utf-8
{"message":"","name":"Error","stack":"stackTraceUsuallyHere"}

But we still get a 500 response! We do see the new CORS headers though.

Sent as a first arg, the response is always a 500. We also don't see the Error message. The stack trace does work though.

Finally, to the recommended method. Here, we should be able to get the best of all methods. When returning an error response from a Twilio function, format the response as the second argument. In that formatting, serialize the error manually. We might create a new helper function:

function formatErrorResponse(statusCode, error) {
  return formatResponse(statusCode, {
    errors: [
      {
        status: statusCode,
        title: error.message,
        meta: {
          stack: error.stack,
        }
      },
    ],
  })
}

Called like this:

const error = new Error('Validation warning')
callback(null, formatErrorResponse(400, error))

It will yield something like:

❯ curl -X POST http://localhost:3000/myfunction -v
< HTTP/1.1 400 Bad Request
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET,PUT,POST,PATCH,DELETE,OPTIONS
< Access-Control-Allow-Headers: Content-Type
< Content-Type: application/json; charset=utf-8
{"errors":[{"status":400,"title":"Validation warning","meta":{"stack":"stackTraceUsuallyHere"}}]}

We did a bit of extra work, creating our own error serialization logic. But that's good. We can be very consistent and make life easier for clients. We can also use a standard response shape, such as jsonapi.

We get the 400 status code. We get the CORS headers. Use this method.

What about you? Have you used this or another method to return error responses from Twilio Functions? What have you discovered?