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

4

8

I have a controller method

// ... inside a controller class

@get('/error', {})
async error() {
  throw new Error("This is the error text");
}

The response I'm getting from this error front-end is:

{ "error": { "statusCode": 500, "message": "Internal Server Error" } }

What I would like the error to be is:

{ "error": { "statusCode": 500, "message": "This is the error text" } }

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

Ponder answered 22/3, 2019 at 22:57 Comment(0)
W
13

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);
  }
}
Wills answered 25/3, 2019 at 7:11 Comment(7)
These are some very interesting details, but I still don't see how this relates to returning errors from within a controller or repository. I don't have access to either a response callback or a reject callback, and throwing does not work. – Ponder
@SephReed I updated my response, I hope it will be more clear now. – Pontus
Hello! But what if I'd like to return a clearer error message, saying to user what happened? – Drivein
@EmilioNumazaki See the line "convert err to plain data object" - that's the place where you can convert the Error object into a user-friendly HTTP response body. – Pontus
This solution is not actual for last versions of Loopback, where default Sequence is not Action Sequence, but its Middleware sequence. – Faires
@Faires Correct. My answer was written before the framework switched to middleware-based sequence by default. – Pontus
There is an HttpErrors class in the @loopback/rest module. Checkout the loopback shopping-card example. It is used quite a bit in there. – Glycosuria
C
4

If you just want to show error message, you just extend Error object and throw it like below. (Loopback documentation didn't mention this anyway)

Avoid using 5xx error and use 4xx error to show some important thing to user is best practice and so that Loopback4 was implemented like this.

class NotFound extends Error {
  statusCode: number

  constructor(message: string) {
    super(message)
    this.statusCode = 404
  }
}

...

if (!await this.userRepository.exists(id)) {
  throw new NotFound('user not found')
}
Cataldo answered 6/1, 2020 at 7:52 Comment(1)
Great! This is the cleanest way to expose exception messages! Thanks! – Drivein
P
2

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"
  }
}
Ponder answered 25/3, 2019 at 15:58 Comment(0)
F
0

To throw custom validation error I use this method:

private static createError(msg: string, name?: string): HttpErrors.HttpError {
    const error = new HttpErrors['422'](msg);
    error.name = name ?? this.name;
    return error;
}

Catch error examples here are for defaultSequence, overriding the handle method. But nowdays app template uses MiddlewareSequence.

So here is the example, how tomodify the response in middleware sequence, you can use this example:

import { Middleware, MiddlewareContext } from '@loopback/rest';

export const ErrorMiddleware: Middleware = async (middlewareCtx: MiddlewareContext, next) => {
    // const {response} = middlewareCtx;
    try {
        // Proceed with next middleware
        return await next();
    } catch (err) {
        // Catch errors from downstream middleware
        // How to catch specific error and how to send custom error response:
        if (HttpErrors.isHttpError(err) || (err as HttpErrors.HttpError).statusCode) {
            const code: string = (err.statusCode || 500).toString();
            if (code.length && code[0] === '4') {
                response.status(Number(code) || 500);
                return response.send({
                    error: {
                        message: err.message,
                        name: err.name || 'UnknownError',
                        statusCode: code
                    }
                });
            }
        }
        throw err;
    }
};

And register the middleware in application.ts

this.middleware(ErrorMiddleware);
Faires answered 4/11, 2021 at 8:24 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.