How do I return an error from a Controller in Loopback 4?

Hello from the LoopBack team

In your controller or repository, you should throw the Error exactly as shown in your question.

Now when LoopBack catches an error, it invokes reject action to handle it. The built-in implementation of reject logs a message via console.error and returns an HTTP response with 4xx/5xx error code and response body describing the error.

By default, LoopBack hides the actual error messages in HTTP responses. This is a security measure preventing the server from leaking potentially sensitive data (paths to files that could not be opened, IP addresses of backend service that could not be reached).

Under the hood, we use strong-error-handler to convert Error objects to HTTP responses. This module offers two modes:

  • Production mode (the default): 5xx errors don't include any additional information, 4xx errors include partial information.
  • Debug mode (debug: true): all error details are included on the response, including a full stack trace.

The debug mode can be enabled by adding the following line to your Application constructor:

this.bind(RestBindings.ERROR_WRITER_OPTIONS).to({debug: true});

Learn more in our docs: Sequence >> Handling errors

Alternatively, you can implement your own error handler and bind it as the sequence action reject. See Customizing sequence actions in our docs.

export class MyRejectProvider implements Provider<Reject> {
  constructor(
    @inject(RestBindings.SequenceActions.LOG_ERROR)
    protected logError: LogError,
    @inject(RestBindings.ERROR_WRITER_OPTIONS, {optional: true})
    protected errorWriterOptions?: ErrorWriterOptions,
  ) {}

  value(): Reject {
    return (context, error) => this.action(context, error);
  }

  action({request, response}: HandlerContext, error: Error) {
    const err = <HttpError>error;

    const statusCode = err.statusCode || err.status || 500;
    const body = // convert err to plain data object

    res.statusCode = statusCode;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.end(JSON.stringify(body), 'utf-8');

    this.logError(error, statusCode, request);
  }
}

For my situation, I found a catch in my sequence.ts file. Inside the catch, it checked if the error had a status code of 4xx, and if not, it just returned a anonymous 500.

Here's the code I was looking for to do the logic:

// sequence.ts
...
} catch (err) {
  console.log(err);
  let code: string = (err.code || 500).toString();
  if (code.length && code[0] === '4') {
    response.status(Number(code) || 500);
    return this.send(response, {
      error: {
        message: err.message,
        name: err.name || 'UnknownError',
        statusCode: code
      }
    });
  }
  return this.reject(context, err);
}
...

Here's how you tell it what to do:

// ... inside a controller class

@get('/error', {})
async error() {
  throw {
    code: 400,
    message: "This is the error text",
    name: "IntentionalError"
  }
}