Handle Errors

The MVC Application or a Controller's error handling varies based on the type of error and its source. In this section you learn how to handle each error depending of its type (e.g. http error like 404, response error from a method or a failure while a method trying to render a response). A Controller can use all the methods at once when it's necessary.

HTTP Errors

Let's start by the most critual one, handle HTTP errors per Controller.

To register an HTTP error handler the Controller (or one of its embedded fields) MUST contain a method named HandleHTTPError. That method can accept and output any type of arguments to render a response, like the rest of the Controller's method. The HandleHTTPError is automatically called by the Framework on HTTP Errors (client 4xx or server 5xx).

Example Code:

The following code snippet will render "errors/404.html" on 404 Not Found, "errors/500.html" on 500 Internal Server Error and e.t.c.

func (c *Base) HandleHTTPError(err mvc.Err, statusCode mvc.Code) mvc.View {
    if err != nil {
        // Do something with that error,
        // e.g. view.Data = MyPageData{Message: err.Error()}
    }

	code := int(statusCode) // cast it to int.

	return mvc.View{
		Code: code,
		Name: fmt.Sprintf("errors/%d.html", code),
	}
}

The input parameter of mvc.Code is optional but a good practise to follow. You could register a Context and get its error code through ctx.GetStatusCode().

This can accept dependencies and output values like any other Controller Method, however be careful if your registered dependencies depend only on successful(200...) requests.

Let's write a method which can fire HandleHTTPError. This is totally optional as HandleHTTPError will be called automatically on HTTP errors (e.g. client 404).

func (c *UserController) GetError() mvc.Result {
	return mvc.View{
		// Map to mvc.Code and mvc.Err respectfully on HandleHTTPError method.
		Code: iris.StatusBadRequest,
		Err:  fmt.Errorf("custom error"),
    }

    // OR mvc.Response{Code: ... Err: ...}
    // OR ctx.StatusCode(...) and ctx.SetErr(...)
    // OR return an (int, error) instead.
}

Also note that, if you register more than one HandleHTTPError in the same Party, you need to use the RouteOverlap feature as shown in the authenticated-controller example.

Custom Errors

A Controller's method can return a custom structure which is responsible to render a response based on given data. This error can be an HTTP Error or a view with 2xx Status OK or a redirect response with 3xx status code.

This can be achieved through a custom mvc.Result or mvc.Preflight (as we've shown at the previous sections).

Using a mvc.Result

Good method to use when mvc.Result is returned from the Controller's method.

Let's create a custom Go structure with some data used to render the response.

type errorResponse struct {
	Code    int
	Message string
}

Implement the mvc.Result through the Dispatch(iris.Context) method.

func (e errorResponse) Dispatch(ctx iris.Context) {
	view := mvc.View{
        Code: e.Code,
        Data: e,
        Name: fmt.Sprintf("errors/%d.html", e.Code),
    }

	view.Dispatch(ctx)
}

Remember: mvc.View, Response and e.t.c are all mvc.Result at the end, so you can call their Dispatch method to render even if a specific method cannot output values (like this Dispatch one).

Alternatively, using just the Context's methods:

func (e errorResponse) Dispatch(ctx iris.Context) {
    viewName := fmt.Sprintf("errors/%d.html", e.Code)

    ctx.StatusCode(e.Code)
    ctx.View(viewName, e)
}

Usage

It's time to use our errorResponse. Let's design a GetBy method which returns mvc.Result, it returns an errorResponse when "user" was not found in our "database".

func (c *UserController) GetBy(id uint64) mvc.Result {
	user, found, err := c.DB.Single(
        "SELECT * FROM users WHERE user_id=? LIMIT 1",
	    id)
	if !found {
		return errorResponse{
			Code:    iris.StatusNotFound,
			Message: "User Not Found",
		}
	}

	// [...]
}

Using a mvc.Preflight

Good method to use when a return value type might be or might not complete mvc.Result or mvc.Preflight interfaces. E.g. a single response which can output data or error. Or even return different types at all, e.g. return a user struct (JSON by default) on valid requests and userError mvc.Preflight on failures. Read more about output values in the dependency injection section.

// Generic response type for JSON results.
type response struct {
	ID        uint64      `json:"id,omitempty"`
	Data      interface{} `json:"data,omitempty"`
	Code      int         `json:"code,omitempty"`
	Message   string      `json:"message,omitempty"`
	Timestamp int64       `json:"timestamp,omitempty"`
}

Complete the mvc.Preflight interface which will run right before the render of response, it can manipulate an object right before it is rendered or handle rendering all by it self by returning the iris.ErrStopExecution error.

func (r response) Preflight(ctx iris.Context) error {
	r.Timestamp = time.Now().Unix()

	if r.Code > 0 {
		// You can call ctx.View or mvc.Vew{...}.Dispatch
		// to render HTML on Code >= 400
		// but in order to not proceed with the response resulting
		// as JSON you MUST return the iris.ErrStopExecution error.
		// Example:
		if r.Code >= 400 && ctx.GetContentTypeRequested() == "text/html" {
			// If code is a client or server error,
			// render a template using mvc.View (you can use ctx.View too).
			mvc.View{
				/* calls the ctx.StatusCode */
				Code: r.Code,
				/* use any r.Data as the template data
				OR the whole "response" as its data. */
				Data: r,
                /* automatically pick the template per error
                   (just for the sake of the example) */
				Name: fmt.Sprintf("errors/%d.html", r.Code),
			}.Dispatch(ctx)

			return iris.ErrStopExecution
		}

		// Else just write the status code based on the given struct's value,
		// see Controller method below.
		ctx.StatusCode(r.Code)
	}

	return nil
}

Usage

Let's use our response type to a Controller's method. The User structure will be embedded into our response.Data field.

type User struct {
	ID uint64 `json:"id"`
}

func (c *Controller) GetBy(userid uint64) response {
    user, found, err := c.DB.Single(
        "SELECT * FROM users WHERE user_id=? LIMIT 1",
        id)
	if !found {
		return response{
			Code:    iris.StatusNotFound,
			Message: "User Not Found",
		}
	}

	return response{
		ID:   userid,
		Data: User{
			ID: userid,
		},
	}
}

Remember: A structure can complete both mvc.Result and mvc.Preflight interfaces. If Preflight does not return iris.ErrStopExecution then it proceeds with the Dispatch (mvc.Result) method one. Here is the underline code.

The above example will render HTML errors when the client accepts html, otherwise will render both error and result as JSON. This method is the recommended one when designing APIs for 3rd-party programmers.

Hijack a Controller Method Result

There is one more option for advanced scenarios. Register a custom ResultHandler when you want to manipulate or change or log each controllers methods return values per MVC Application or globally.

Example Code:

package main

import (
	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/mvc"
)

func main() {
	app := iris.New()
	app.RegisterView(iris.HTML("./views", ".html"))

	// Hijack each output value of a method (can be used per-party too).
	app.ConfigureContainer().
		UseResultHandler(func(next iris.ResultHandler) iris.ResultHandler {
			return func(ctx iris.Context, v interface{}) error {
				switch val := v.(type) {
				case errorResponse:
					return next(ctx, errorView(val))
				default:
					return next(ctx, v)
				}
			}
		})

	m := mvc.New(app)
	m.Handle(new(controller))

	app.Listen(":8080")
}

func errorView(e errorResponse) mvc.Result {
	switch e.Code {
	case iris.StatusNotFound:
		return mvc.View{Code: e.Code, Name: "404.html", Data: e}
	default:
		return mvc.View{Code: e.Code, Name: "500.html", Data: e}
	}
}

type errorResponse struct {
	Code    int
	Message string
}

type controller struct{}

type user struct {
	ID uint64 `json:"id"`
}

func (c *controller) GetBy(userid uint64) interface{} {
	if userid != 1 {
		return errorResponse{
			Code:    iris.StatusNotFound,
			Message: "User Not Found",
		}
	}

	return user{ID: userid}
}

Last updated